From abf83097f2a4bbbea6724865a2087c89a2e93ffe Mon Sep 17 00:00:00 2001 From: Kamforka Date: Thu, 2 May 2024 15:03:51 +0200 Subject: [PATCH] 324 - Use the official thehive v5.3.0 for integration tests --- README.md | 31 ++++++++++++--- tests/conftest.py | 10 ++--- tests/test_case_endpoint.py | 2 + tests/test_custom_field_endpoint.py | 3 +- tests/test_user_endpoint.py | 1 + tests/utils.py | 60 ++++++++++++++++++++++++----- 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8adf9d4..5f9cf82 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ If you are a first time contributor to github projects please make yourself comf Navigate to the cloned repository's directory and install the package with development extras using pip: ``` -pip install -e '.[dev]' +pip install -e .[dev] ``` This command installs the package in editable mode (`-e`) and includes additional development dependencies. @@ -304,14 +304,33 @@ With pre-commit hooks in place, your changes will be automatically validated for ## Testing +> IMPORTANT NOTE: Since TheHive 5.3 the licensing constraints has been partially lifted therefore a public integrator image is available for running tests both locally and in github. + `thehive4py` primarily relies on integration tests, which are designed to execute against a live TheHive 5.x instance. These tests ensure that the library functions correctly in an environment closely resembling real-world usage. -However, due to licensing constraints with TheHive 5.x, the integration tests are currently not available for public or local use. +### Test requirements + +Since the test suite relies on the existence of a live TheHive docker container a local docker engine installation is a must. +If you are unfamiliar with docker please check out the [official documentation][get-docker]. + +### Test setup + +The test suite relies on the official [thehive-image] to create a container locally with the predefined name `thehive4py-integration-tester` which will act as a unique id. +The container will expose TheHive on a random port to make sure it causes no conflicts for any other containers which expose ports. +The suite can identify this random port by querying the container info based on the predefined name. +Once TheHive is responsive the suite will initialize the instance with a setup required by the tests (e.g.: test users, organisations, etc.). +Please note that due to this initial setup the very first test run will idle for some time to make sure everything is up and running. Any other subsequent runs' statup time should be significantly faster. + +### Testing locally +To execute the whole test suite locally one can use the `scripts/ci.py` utility script like: -To ensure code quality and prevent broken code from being merged, a private image is available for the integration-test workflow. This means that any issues should be detected and addressed during the PR phase. + ./scripts/ci.py --test -The project is actively working on a solution to enable developers to run integration tests locally, providing a more accessible and comprehensive testing experience. +Note however that the above will execute the entire test suite which can take several minutes to complete. +In case one wants to execute only a portion of the test suite then the easiest workaround is to use `pytest` and pass the path to the specific test module. For example to only execute tests for the alert endpoints one can do: -While local testing is in development, relying on the automated PR checks ensures the reliability and quality of the `thehive4py` library. + pytest -v tests/test_alert_endpoint.py -[query-api-docs]: https://docs.strangebee.com/thehive/api-docs/#operation/Query%20API \ No newline at end of file +[get-docker]: https://docs.docker.com/get-docker/ +[query-api-docs]: https://docs.strangebee.com/thehive/api-docs/#operation/Query%20API +[thehive-image]: https://hub.docker.com/r/strangebee/thehive \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 648005f..859d96c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest -from tests.utils import TestConfig, reinit_hive_container, spawn_hive_container +from tests.utils import TestConfig, reset_hive_instance, spawn_hive_container from thehive4py.client import TheHiveApi from thehive4py.helpers import now_to_ts from thehive4py.types.alert import InputAlert, OutputAlert @@ -23,8 +23,8 @@ @pytest.fixture(scope="session") def test_config(): return TestConfig( - image_name="kamforka/thehive4py-integrator:thehive-5.2.11", - container_name="thehive4py-integration-tests", + image_name="strangebee/thehive:5.3.0", + container_name="thehive4py-integration-tester", user="admin@thehive.local", password="secret", admin_org="admin", @@ -34,8 +34,8 @@ def test_config(): @pytest.fixture(scope="function", autouse=True) -def init_hive_container(test_config: TestConfig): - reinit_hive_container(test_config=test_config) +def auto_reset_hive_instance(thehive: TheHiveApi, test_config: TestConfig): + reset_hive_instance(hive_url=thehive.session.hive_url, test_config=test_config) @pytest.fixture(scope="session") diff --git a/tests/test_case_endpoint.py b/tests/test_case_endpoint.py index 712097e..279831e 100644 --- a/tests/test_case_endpoint.py +++ b/tests/test_case_endpoint.py @@ -197,6 +197,7 @@ def test_share_and_unshare(self, thehive: TheHiveApi, test_case: OutputCase): thehive.case.unshare(case_id=test_case["_id"], organisation_ids=[organisation]) assert len(thehive.case.list_shares(case_id=test_case["_id"])) == 1 + @pytest.mark.skip(reason="integrator container only supports a single org ") def test_share_and_remove_share(self, thehive: TheHiveApi, test_case: OutputCase): organisation = "share-org" share: InputShare = {"organisation": organisation} @@ -220,6 +221,7 @@ def test_update_share(self, thehive: TheHiveApi, test_case: OutputCase): updated_share = thehive.case.share(case_id=test_case["_id"], shares=[share])[0] assert updated_share["profileName"] == update_profile + @pytest.mark.skip(reason="integrator container only supports a single org ") def test_share_and_set_share(self, thehive: TheHiveApi, test_case: OutputCase): organisation = "share-org" share: InputShare = {"organisation": organisation} diff --git a/tests/test_custom_field_endpoint.py b/tests/test_custom_field_endpoint.py index aad2377..b355fe7 100644 --- a/tests/test_custom_field_endpoint.py +++ b/tests/test_custom_field_endpoint.py @@ -1,10 +1,11 @@ import pytest + from thehive4py.client import TheHiveApi from thehive4py.errors import TheHiveError from thehive4py.types.custom_field import InputUpdateCustomField, OutputCustomField -class TestCustomeFieldEndpoint: +class TestCustomFieldEndpoint: def test_create_and_list(self, thehive_admin: TheHiveApi): created_custom_field = thehive_admin.custom_field.create( custom_field={ diff --git a/tests/test_user_endpoint.py b/tests/test_user_endpoint.py index 289a3e7..68bbfd2 100644 --- a/tests/test_user_endpoint.py +++ b/tests/test_user_endpoint.py @@ -61,6 +61,7 @@ def test_delete(self, thehive: TheHiveApi, test_user: OutputUser): with pytest.raises(TheHiveError): thehive.user.get(user_id=user_id) + @pytest.mark.skip(reason="integrator container only supports a single org ") def test_set_organisations( self, test_config: TestConfig, thehive: TheHiveApi, test_user: OutputUser ): diff --git a/tests/utils.py b/tests/utils.py index 750e94a..d7709f7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,7 @@ import requests from thehive4py.client import TheHiveApi +from thehive4py.helpers import now_to_ts from thehive4py.query.filters import Eq @@ -27,7 +28,7 @@ class TestConfig: def _is_container_responsive(container_url: str) -> bool: COOLDOWN = 1.0 - TIMEOUT = 60.0 + TIMEOUT = 120.0 now = time.time() end = now + TIMEOUT @@ -85,7 +86,7 @@ def _destroy_container(container_name: str): ) -def _reinit_hive_org(hive_url: str, test_config: TestConfig, organisation: str) -> None: +def _reset_hive_org(hive_url: str, test_config: TestConfig, organisation: str) -> None: client = TheHiveApi( url=hive_url, username=test_config.user, @@ -101,7 +102,7 @@ def _reinit_hive_org(hive_url: str, test_config: TestConfig, organisation: str) executor.map(client.case.delete, [case["_id"] for case in cases]) -def _reinit_hive_admin_org(hive_url: str, test_config: TestConfig) -> None: +def _reset_hive_admin_org(hive_url: str, test_config: TestConfig) -> None: client = TheHiveApi( url=hive_url, username=test_config.user, @@ -129,6 +130,45 @@ def _reinit_hive_admin_org(hive_url: str, test_config: TestConfig) -> None: ) +def init_hive_instance(url: str, test_config: TestConfig): + hive = TheHiveApi( + url=url, + username=test_config.user, + password=test_config.password, + organisation="admin", + ) + + current_user = hive.user.get_current() + + current_license = hive.session.make_request("GET", "/api/v1/license/current") + if current_license["fallback"]["expiresAt"] < now_to_ts(): + _destroy_container(container_name=test_config.container_name) + spawn_hive_container(test_config=test_config) + + if not len(hive.organisation.find(filters=Eq("name", test_config.main_org))): + hive.organisation.create( + organisation={ + "name": test_config.main_org, + "description": "main organisation for testing", + } + ) + + hive.user.set_organisations( + user_id=current_user["_id"], + organisations=[ + { + "organisation": test_config.main_org, + "profile": "org-admin", + "default": True, + }, + { + "organisation": "admin", + "profile": "admin", + }, + ], + ) + + def spawn_hive_container(test_config: TestConfig) -> str: if not _is_container_exist(container_name=test_config.container_name): _run_container( @@ -141,15 +181,17 @@ def spawn_hive_container(test_config: TestConfig) -> str: _destroy_container(container_name=test_config.container_name) raise RuntimeError("Unable to startup test container for TheHive") + init_hive_instance(url=url, test_config=test_config) + return url -def reinit_hive_container(test_config: TestConfig) -> None: - hive_url = spawn_hive_container(test_config=test_config) +def reset_hive_instance(hive_url: str, test_config: TestConfig) -> None: + # TODO: add back share config reinitialization once the license allows it with ThreadPoolExecutor() as executor: - for organisation in [ + for org in [ test_config.main_org, - test_config.share_org, + # test_config.share_org, ]: - executor.submit(_reinit_hive_org, hive_url, test_config, organisation) - executor.submit(_reinit_hive_admin_org, hive_url, test_config) + executor.submit(_reset_hive_org, hive_url, test_config, org) + executor.submit(_reset_hive_admin_org, hive_url, test_config)