diff --git a/tests/conftest.py b/tests/conftest.py index b444bcf7..dc40e52d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,7 @@ import argparse import logging -import os import pathlib -import subprocess from tempfile import TemporaryDirectory from typing import Any, Dict, Generator, Tuple, TypeVar @@ -17,15 +15,7 @@ from trestle.common.err import TrestleError from trestle.core.commands.init import InitCmd -from tests.testutils import ( - CONTAINER_FILE_NAME, - E2E_BUILD_CONTEXT, - MOCK_SERVER_IMAGE_NAME, - TRESTLEBOT_TEST_IMAGE_NAME, - build_test_image, - clean, - repo_setup, -) +from tests.testutils import clean, repo_setup from trestlebot import const from trestlebot.transformers.trestle_rule import ( Check, @@ -196,45 +186,3 @@ def test_valid_csv_row() -> Dict[str, str]: "Profile_Source": "test", "Namespace": "test", } - - -# E2E test fixtures - - -@pytest.fixture(scope="package") -def podman_setup() -> YieldFixture[Tuple[int, str]]: - """ - Build the trestlebot container image and run the mock server in a pod. - - Yields: - Tuple[int, str]: The return code from the podman play command and the trestlebot image name. - """ - - # Get the image information from the environment, if present - trestlebot_image = os.environ.get("TRESTLEBOT_IMAGE", TRESTLEBOT_TEST_IMAGE_NAME) - - cleanup_trestlebot_image = build_test_image(trestlebot_image) - cleanup_mock_server_image = build_test_image( - MOCK_SERVER_IMAGE_NAME, - f"{E2E_BUILD_CONTEXT}/{CONTAINER_FILE_NAME}", - E2E_BUILD_CONTEXT, - ) - - # Create a pod - response = subprocess.run( - ["podman", "play", "kube", f"{E2E_BUILD_CONTEXT}/play-kube.yml"], check=True - ) - yield response.returncode, trestlebot_image - - # Clean up the container image, pod and mock server - try: - subprocess.run( - ["podman", "play", "kube", "--down", f"{E2E_BUILD_CONTEXT}/play-kube.yml"], - check=True, - ) - if cleanup_trestlebot_image: - subprocess.run(["podman", "rmi", trestlebot_image], check=True) - if cleanup_mock_server_image: - subprocess.run(["podman", "rmi", MOCK_SERVER_IMAGE_NAME], check=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to clean up podman resources: {e}") diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 00000000..84b2b506 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Red Hat, Inc. + + +"""E2E test fixtures.""" + +import pytest + +from tests.conftest import YieldFixture +from tests.e2e.e2e_testutils import E2ETestRunner + + +@pytest.fixture(scope="package") +def e2e_runner() -> YieldFixture[E2ETestRunner]: + """Fixture for running e2e tests.""" + runner = E2ETestRunner() + runner.setup() + yield runner + runner.teardown() diff --git a/tests/e2e/e2e_testutils.py b/tests/e2e/e2e_testutils.py new file mode 100644 index 00000000..d5ac7f52 --- /dev/null +++ b/tests/e2e/e2e_testutils.py @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Red Hat, Inc. + + +"""Helper functions and class for e2e setup, execution, and teardown.""" + +import os +import pathlib +import subprocess +from typing import Dict, List, Optional, Tuple + +from tests.testutils import args_dict_to_list + + +class E2ETestRunner: + """Class to run e2e tests.""" + + TRESTLEBOT_TEST_IMAGE_NAME = "localhost/trestlebot:latest" + MOCK_SERVER_IMAGE_NAME = "localhost/mock-server:latest" + TRESTLEBOT_TEST_POD_NAME = "trestlebot-e2e-pod" + E2E_BUILD_CONTEXT = "tests/e2e" + CONTAINER_FILE_NAME = "Dockerfile" + UPSTREAM_REPO = "/upstream" + + def __init__(self) -> None: + """Initialize the class.""" + self.trestlebot_image = os.environ.get( + "TRESTLEBOT_IMAGE", E2ETestRunner.TRESTLEBOT_TEST_IMAGE_NAME + ) + self.cleanup_trestlebot_image = False + self.cleanup_mock_server_image = False + + def setup(self) -> None: + """ + Build the trestlebot container image and run the mock server in a pod. + + Yields: + Tuple[int, str]: The return code from the podman play command and the trestlebot image name. + """ + try: + self.cleanup_trestlebot_image = self.build_test_image(self.trestlebot_image) + self.cleanup_mock_server_image = self.build_test_image( + E2ETestRunner.MOCK_SERVER_IMAGE_NAME, + f"{E2ETestRunner.E2E_BUILD_CONTEXT}/{E2ETestRunner.CONTAINER_FILE_NAME}", + E2ETestRunner.E2E_BUILD_CONTEXT, + ) + + # Create a pod + subprocess.run( + [ + "podman", + "play", + "kube", + f"{E2ETestRunner.E2E_BUILD_CONTEXT}/play-kube.yml", + ], + check=True, + ) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to set up podman resources: {e}") + + def teardown(self) -> None: + """Clean up the container image, pod and mock server""" + try: + subprocess.run( + [ + "podman", + "play", + "kube", + "--down", + f"{E2ETestRunner.E2E_BUILD_CONTEXT}/play-kube.yml", + ], + check=True, + ) + if self.cleanup_trestlebot_image: + subprocess.run(["podman", "rmi", self.trestlebot_image], check=True) + if self.cleanup_mock_server_image: + subprocess.run( + ["podman", "rmi", E2ETestRunner.MOCK_SERVER_IMAGE_NAME], check=True + ) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to clean up podman resources: {e}") + + @staticmethod + def _image_exists(image_name: str) -> bool: + """Check if the image already exists.""" + try: + subprocess.check_output(["podman", "image", "inspect", image_name]) + return True + except subprocess.CalledProcessError: + return False + + def build_test_image( + self, + image_name: str, + container_file: str = CONTAINER_FILE_NAME, + build_context: str = ".", + ) -> bool: + """ + Build an image for testing image. + + Returns: + Returns true if the image was built, false if it already exists. + """ + if not self._image_exists(image_name): + subprocess.run( + [ + "podman", + "build", + "-f", + container_file, + "-t", + image_name, + build_context, + ], + check=True, + ) + return True + return False + + def build_test_command( + self, + data_path: str, + command_name: str, + command_args: Dict[str, str], + upstream_repo: str = "", + ) -> List[str]: + """ + Build a command to be run in the shell for trestlebot + + Args: + data_path (str): Path to the data directory. This is the working directory/trestle_root. + command_name (str): Name of the command to run. It should be a trestlebot command. + command_args (Dict[str, str]): Arguments to pass to the command + image_name (str, optional): Name of the image to run. Defaults to TRESTLEBOT_TEST_IMAGE_NAME. + upstream_repo (str, optional): Path to the upstream repo. Defaults to "" and is not mounted. + + Returns: + List[str]: Command to be run in the shell + """ + command = [ + "podman", + "run", + "--pod", + E2ETestRunner.TRESTLEBOT_TEST_POD_NAME, + "--entrypoint", + f"trestlebot-{command_name}", + "--rm", + ] + + # Add mounts + if upstream_repo: + # Add a volume and mount it to the container + command.extend(["-v", f"{upstream_repo}:{E2ETestRunner.UPSTREAM_REPO}"]) + + command.extend( + [ + "-v", + f"{data_path}:/trestle", + "-w", + "/trestle", + self.trestlebot_image, + *args_dict_to_list(command_args), + ] + ) + return command + + def invoke_command( + self, command: List[str], working_dir: Optional[pathlib.Path] = None + ) -> Tuple[int, str]: + """ + Invoke a command in the e2e test. + + Args: + command (str): Command to run in the shell + + Returns: + Tuple[int, str]: Return code and stdout of the command + """ + result = subprocess.run(command, cwd=working_dir, capture_output=True) + return result.returncode, result.stdout.decode("utf-8") diff --git a/tests/e2e/test_e2e_compdef.py b/tests/e2e/test_e2e_compdef.py index 405dc25d..b25622be 100644 --- a/tests/e2e/test_e2e_compdef.py +++ b/tests/e2e/test_e2e_compdef.py @@ -6,8 +6,7 @@ import logging import pathlib -import subprocess -from typing import Dict, Tuple +from typing import Dict, List, Tuple import pytest from git.repo import Repo @@ -16,18 +15,9 @@ from trestle.oscal.component import ComponentDefinition from trestle.oscal.profile import Profile -from tests.testutils import ( - build_test_command, - load_from_json, - setup_for_profile, - setup_rules_view, -) -from trestlebot.const import ( - ERROR_EXIT_CODE, - INVALID_ARGS_EXIT_CODE, - RULES_VIEW_DIR, - SUCCESS_EXIT_CODE, -) +from tests.e2e.e2e_testutils import E2ETestRunner +from tests.testutils import load_from_json, setup_for_profile, setup_rules_view +from trestlebot.const import RULES_VIEW_DIR, SUCCESS_EXIT_CODE logger = logging.getLogger(__name__) @@ -40,7 +30,7 @@ @pytest.mark.slow @pytest.mark.parametrize( - "test_name, command_args, response", + "test_name, command_args", [ ( "success/happy path", @@ -50,7 +40,6 @@ "committer-name": "test", "committer-email": "test@email.com", }, - SUCCESS_EXIT_CODE, ), ( "success/happy path with model skipping", @@ -61,67 +50,47 @@ "committer-email": "test", "skip-items": test_comp_name, }, - SUCCESS_EXIT_CODE, - ), - ( - "failure/missing args", - { - "branch": "test", - "rules-view-path": RULES_VIEW_DIR, - }, - INVALID_ARGS_EXIT_CODE, ), ], ) def test_rules_transform_e2e( tmp_repo: Tuple[str, Repo], - podman_setup: Tuple[int, str], + e2e_runner: E2ETestRunner, test_name: str, command_args: Dict[str, str], - response: int, ) -> None: """Test the trestlebot rules transform command.""" - # Check that the container image was built successfully - # and the mock server is running - exit_code, image_name = podman_setup - assert exit_code == 0 - logger.info(f"Running test: {test_name}") - tmp_repo_str, repo = tmp_repo + tmp_repo_str, _ = tmp_repo tmp_repo_path = pathlib.Path(tmp_repo_str) # Setup the rules directory setup_rules_view(tmp_repo_path, test_comp_name) - command = build_test_command( - tmp_repo_str, "rules-transform", command_args, image_name + command: List[str] = e2e_runner.build_test_command( + tmp_repo_str, "rules-transform", command_args ) - run_response = subprocess.run(command, capture_output=True) - assert run_response.returncode == response + exit_code, response_stdout = e2e_runner.invoke_command(command) + assert exit_code == SUCCESS_EXIT_CODE # Check that the component definition was created - if response == SUCCESS_EXIT_CODE: + if exit_code == SUCCESS_EXIT_CODE: if "skip-items" in command_args: - assert f"input: {test_comp_name}.csv" not in run_response.stdout.decode( - "utf-8" - ) + assert f"input: {test_comp_name}.csv" not in response_stdout else: comp_path: pathlib.Path = ModelUtils.get_model_path_for_name_and_class( tmp_repo_path, test_comp_name, ComponentDefinition, FileContentType.JSON ) assert comp_path.exists() - assert f"input: {test_comp_name}.csv" in run_response.stdout.decode("utf-8") + assert f"input: {test_comp_name}.csv" in response_stdout branch = command_args["branch"] - assert ( - f"Changes pushed to {branch} successfully." - in run_response.stdout.decode("utf-8") - ) + assert f"Changes pushed to {branch} successfully." in response_stdout @pytest.mark.slow @pytest.mark.parametrize( - "test_name, command_args, response", + "test_name, command_args", [ ( "success/happy path", @@ -135,7 +104,6 @@ def test_rules_transform_e2e( "committer-name": "test", "committer-email": "test@email.com", }, - SUCCESS_EXIT_CODE, ), ( "success/happy path with filtering", @@ -150,65 +118,16 @@ def test_rules_transform_e2e( "committer-email": "test@email.com", "filter-by-profile": test_filter_prof, }, - SUCCESS_EXIT_CODE, - ), - ( - "failure/missing args", - { - "component-title": "test-comp", - "compdef-name": "test-compdef", - "component-description": "test", - "markdown-path": "markdown", - "branch": "test", - "committer-name": "test", - "committer-email": "test@email.com", - }, - INVALID_ARGS_EXIT_CODE, - ), - ( - "failure/missing profile", - { - "profile-name": "fake", - "component-title": "test-comp", - "compdef-name": "test-compdef", - "component-description": "test", - "markdown-path": "markdown", - "branch": "test", - "committer-name": "test", - "committer-email": "test@email.com", - }, - ERROR_EXIT_CODE, - ), - ( - "failure/missing filter profile", - { - "profile-name": test_prof, - "component-title": "test-comp", - "compdef-name": "test-compdef", - "component-description": "test", - "markdown-path": "markdown", - "branch": "test", - "committer-name": "test", - "committer-email": "test", - "filter-by-profile": "fake", - }, - ERROR_EXIT_CODE, ), ], ) def test_create_cd_e2e( tmp_repo: Tuple[str, Repo], - podman_setup: Tuple[int, str], + e2e_runner: E2ETestRunner, test_name: str, command_args: Dict[str, str], - response: int, ) -> None: """Test the trestlebot create-cd command.""" - # Check that the container image was built successfully - # and the mock server is running - exit_code, image_name = podman_setup - assert exit_code == 0 - logger.info(f"Running test: {test_name}") tmp_repo_str, _ = tmp_repo @@ -218,23 +137,22 @@ def test_create_cd_e2e( _ = setup_for_profile(tmp_repo_path, test_prof, "") load_from_json(tmp_repo_path, test_filter_prof, test_filter_prof, Profile) - command = build_test_command(tmp_repo_str, "create-cd", command_args, image_name) - run_response = subprocess.run(command, cwd=tmp_repo_path, capture_output=True) - assert run_response.returncode == response + command = e2e_runner.build_test_command(tmp_repo_str, "create-cd", command_args) + exit_code, _ = e2e_runner.invoke_command(command, tmp_repo_path) + assert exit_code == SUCCESS_EXIT_CODE # Check that all expected files were created - if response == SUCCESS_EXIT_CODE: - comp_path: pathlib.Path = ModelUtils.get_model_path_for_name_and_class( - tmp_repo_path, - command_args["compdef-name"], - ComponentDefinition, - FileContentType.JSON, - ) - assert comp_path.exists() - assert (tmp_repo_path / command_args["markdown-path"]).exists() - assert ( - tmp_repo_path - / RULES_VIEW_DIR - / command_args["compdef-name"] - / command_args["component-title"] - ).exists() + comp_path: pathlib.Path = ModelUtils.get_model_path_for_name_and_class( + tmp_repo_path, + command_args["compdef-name"], + ComponentDefinition, + FileContentType.JSON, + ) + assert comp_path.exists() + assert (tmp_repo_path / command_args["markdown-path"]).exists() + assert ( + tmp_repo_path + / RULES_VIEW_DIR + / command_args["compdef-name"] + / command_args["component-title"] + ).exists() diff --git a/tests/e2e/test_e2e_ssp.py b/tests/e2e/test_e2e_ssp.py index ff91ce64..06695cb9 100644 --- a/tests/e2e/test_e2e_ssp.py +++ b/tests/e2e/test_e2e_ssp.py @@ -29,13 +29,8 @@ from trestle.core.models.file_content_type import FileContentType from trestle.oscal.ssp import SystemSecurityPlan -from tests.testutils import ( - UPSTREAM_REPO, - build_test_command, - clean, - prepare_upstream_repo, - setup_for_ssp, -) +from tests.e2e.e2e_testutils import E2ETestRunner +from tests.testutils import clean, prepare_upstream_repo, setup_for_ssp from trestlebot.const import ERROR_EXIT_CODE, SUCCESS_EXIT_CODE from trestlebot.tasks.authored.ssp import SSPIndex @@ -82,18 +77,13 @@ ) def test_ssp_editing_e2e( tmp_repo: Tuple[str, Repo], - podman_setup: Tuple[int, str], + e2e_runner: E2ETestRunner, test_name: str, command_args: Dict[str, str], response: int, skip_create: bool, ) -> None: """Test the trestlebot autosync command with SSPs.""" - # Check that the container image was built successfully - # and the mock server is running - exit_code, image_name = podman_setup - assert exit_code == 0 - logger.info(f"Running test: {test_name}") tmp_repo_str, _ = tmp_repo @@ -112,11 +102,13 @@ def test_ssp_editing_e2e( "profile-name": test_prof, "compdefs": test_comp_name, } - command = build_test_command( - tmp_repo_str, "create-ssp", create_args, image_name + command = e2e_runner.build_test_command( + tmp_repo_str, + "create-ssp", + create_args, ) - run_response = subprocess.run(command, capture_output=True) - assert run_response.returncode == response + exit_code, _ = e2e_runner.invoke_command(command) + assert exit_code == response assert (tmp_repo_path / command_args["markdown-path"]).exists() # Make a change to the SSP @@ -132,7 +124,7 @@ def test_ssp_editing_e2e( ssp_generate = SSPGenerate() assert ssp_generate._run(args) == 0 - command = build_test_command(tmp_repo_str, "autosync", command_args, image_name) + command = e2e_runner.build_test_command(tmp_repo_str, "autosync", command_args) run_response = subprocess.run(command, capture_output=True) assert run_response.returncode == response @@ -153,41 +145,40 @@ def test_ssp_editing_e2e( assert ssp_path.exists() # Check that if run again, the ssp is not pushed again - command = build_test_command(tmp_repo_str, "autosync", command_args, image_name) - run_response = subprocess.run(command, capture_output=True) - assert run_response.returncode == SUCCESS_EXIT_CODE - assert "Nothing to commit" in run_response.stdout.decode("utf-8") + command = e2e_runner.build_test_command(tmp_repo_str, "autosync", command_args) + exit_code, response_stdout = e2e_runner.invoke_command(command) + assert exit_code == SUCCESS_EXIT_CODE + assert "Nothing to commit" in response_stdout # Check that if the upstream profile is updated, the ssp is updated local_upstream_path = prepare_upstream_repo() - upstream_repos_arg = f"{UPSTREAM_REPO}@main" + upstream_repos_arg = f"{e2e_runner.UPSTREAM_REPO}@main" upstream_command_args = { "branch": command_args["branch"], "committer-name": command_args["committer-name"], "committer-email": command_args["committer-email"], "sources": upstream_repos_arg, } - command = build_test_command( + command = e2e_runner.build_test_command( tmp_repo_str, "sync-upstreams", upstream_command_args, - image_name, local_upstream_path, ) - run_response = subprocess.run(command, capture_output=True) - assert run_response.returncode == SUCCESS_EXIT_CODE + exit_code, response_stdout = e2e_runner.invoke_command(command) + assert exit_code == SUCCESS_EXIT_CODE assert ( f"Changes pushed to {command_args['branch']} successfully." in run_response.stdout.decode("utf-8") ) # Autosync again to check that the ssp is updated - command = build_test_command(tmp_repo_str, "autosync", command_args, image_name) - run_response = subprocess.run(command, capture_output=True) - assert run_response.returncode == SUCCESS_EXIT_CODE + command = e2e_runner.build_test_command(tmp_repo_str, "autosync", command_args) + exit_code, response_stdout = e2e_runner.invoke_command(command) + assert exit_code == SUCCESS_EXIT_CODE assert ( f"Changes pushed to {command_args['branch']} successfully." - in run_response.stdout.decode("utf-8") + in response_stdout ) # Clean up the upstream repo diff --git a/tests/testutils.py b/tests/testutils.py index bd757552..525f3331 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -7,7 +7,6 @@ import argparse import pathlib import shutil -import subprocess import tempfile from typing import Dict, List, Optional @@ -30,15 +29,6 @@ INVALID_TEST_SSP_INDEX = JSON_TEST_DATA_PATH / "invalid_test_ssp_index.json" TEST_YAML_HEADER = YAML_TEST_DATA_PATH / "extra_yaml_header.yaml" -# E2E test constants -TRESTLEBOT_TEST_IMAGE_NAME = "localhost/trestlebot:latest" -MOCK_SERVER_IMAGE_NAME = "localhost/mock-server:latest" -TRESTLEBOT_TEST_POD_NAME = "trestlebot-e2e-pod" -E2E_BUILD_CONTEXT = "tests/e2e" -CONTAINER_FILE_NAME = "Dockerfile" - -# Location the upstream repo is mounted to in the container -UPSTREAM_REPO = "/upstream" TEST_REMOTE_REPO_URL = "http://localhost:8080/test.git" @@ -281,90 +271,6 @@ def replace_string_in_file(file_path: str, old_string: str, new_string: str) -> file.write(updated_content) -def _image_exists(image_name: str) -> bool: - """Check if the image already exists.""" - try: - subprocess.check_output(["podman", "image", "inspect", image_name]) - return True - except subprocess.CalledProcessError: - return False - - -def build_test_image( - image_name: str, - container_file: str = CONTAINER_FILE_NAME, - build_context: str = ".", -) -> bool: - """ - Build an image for testing image. - - Returns: - Returns true if the image was built, false if it already exists. - """ - if not _image_exists(image_name): - subprocess.run( - [ - "podman", - "build", - "-f", - container_file, - "-t", - image_name, - build_context, - ], - check=True, - ) - return True - return False - - -def build_test_command( - data_path: str, - command_name: str, - command_args: Dict[str, str], - image_name: str = TRESTLEBOT_TEST_IMAGE_NAME, - upstream_repo: str = "", -) -> List[str]: - """ - Build a command to be run in the shell for trestlebot - - Args: - data_path (str): Path to the data directory. This is the working directory/trestle_root. - command_name (str): Name of the command to run. It should be a trestlebot command. - command_args (Dict[str, str]): Arguments to pass to the command - image_name (str, optional): Name of the image to run. Defaults to TRESTLEBOT_TEST_IMAGE_NAME. - upstream_repo (str, optional): Path to the upstream repo. Defaults to "" and is not mounted. - - Returns: - List[str]: Command to be run in the shell - """ - command = [ - "podman", - "run", - "--pod", - TRESTLEBOT_TEST_POD_NAME, - "--entrypoint", - f"trestlebot-{command_name}", - "--rm", - ] - - # Add mounts - if upstream_repo: - # Add a volume and mount it to the container - command.extend(["-v", f"{upstream_repo}:{UPSTREAM_REPO}"]) - command.extend( - [ - "-v", - f"{data_path}:/trestle", - "-w", - "/trestle", - image_name, - *args_dict_to_list(command_args), - ] - ) - return command - - def prepare_upstream_repo() -> str: """ Prepare a temporary upstream repo for testing. diff --git a/tests/trestlebot/entrypoints/test_create_cd.py b/tests/trestlebot/entrypoints/test_create_cd.py new file mode 100644 index 00000000..bf539897 --- /dev/null +++ b/tests/trestlebot/entrypoints/test_create_cd.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Red Hat, Inc. + + +"""Test for Create CD CLI""" + +import logging +import pathlib +from typing import Any, Dict +from unittest.mock import patch + +import pytest + +from tests.testutils import args_dict_to_list, setup_for_compdef +from trestlebot.entrypoints.create_cd import main as cli_main + + +@pytest.fixture +def valid_args_dict() -> Dict[str, str]: + return { + "profile-name": "simplified_nist_profile", + "component-title": "test-comp", + "compdef-name": "test-compdef", + "component-description": "test", + "markdown-path": "markdown", + "branch": "test", + "committer-name": "test", + "committer-email": "test@email.com", + } + + +test_comp_name = "test_comp" +test_ssp_cd = "md_cd" + + +def test_create_cd_with_missing_args( + tmp_trestle_dir: str, valid_args_dict: Dict[str, str] +) -> None: + """Test create cd and trigger error.""" + tmp_repo_path = pathlib.Path(tmp_trestle_dir) + + args_dict = valid_args_dict + del args_dict["profile-name"] + + _ = setup_for_compdef(tmp_repo_path, test_comp_name, test_ssp_cd) + + with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]): + with pytest.raises(SystemExit, match="2"): + cli_main() + + +def test_create_cd_with_missing_profile( + tmp_trestle_dir: str, valid_args_dict: Dict[str, str], caplog: Any +) -> None: + """Test create cd and trigger error.""" + tmp_repo_path = pathlib.Path(tmp_trestle_dir) + + args_dict = valid_args_dict + args_dict["profile-name"] = "invalid_prof" + args_dict["working-dir"] = tmp_trestle_dir + + _ = setup_for_compdef(tmp_repo_path, test_comp_name, test_ssp_cd) + + with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]): + with pytest.raises(SystemExit, match="1"): + cli_main() + + assert any( + record.levelno == logging.ERROR + and "Profile invalid_prof does not exist in the workspace" in record.message + for record in caplog.records + ) + + +def test_create_cd_with_missing_filter_profile( + tmp_trestle_dir: str, valid_args_dict: Dict[str, str], caplog: Any +) -> None: + """Test create cd and trigger error.""" + tmp_repo_path = pathlib.Path(tmp_trestle_dir) + + args_dict = valid_args_dict + args_dict["filter-by-profile"] = "invalid_prof" + args_dict["working-dir"] = tmp_trestle_dir + + _ = setup_for_compdef(tmp_repo_path, test_comp_name, test_ssp_cd) + + with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]): + with pytest.raises(SystemExit, match="1"): + cli_main() + + assert any( + record.levelno == logging.ERROR + and "Profile invalid_prof does not exist in the workspace" in record.message + for record in caplog.records + )