Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework test container #326

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_build-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
python-version: 3.12
- name: Install build dependencies
run: pip install --no-cache-dir -U pip .['build']
- name: Build package
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/_integration-tests.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: integration-tests
on:
workflow_call:
secrets:
DOCKER_TOKEN:
required: true
jobs:
integration-tests:
name: Run integration tests
Expand All @@ -13,10 +10,8 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
python-version: 3.12
- name: Install dependencies
run: pip install --no-cache-dir -U pip .['test']
- name: Docker login
run: docker login -u kamforka -p ${{ secrets.DOCKER_TOKEN }}
- name: Run integration tests
run: scripts/ci.py --test
2 changes: 1 addition & 1 deletion .github/workflows/_static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/_upload-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
python-version: 3.12
- name: Install build dependencies
run: pip install --no-cache-dir -U pip .['build']
- name: Upload to PyPI
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/main-cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ jobs:
uses: ./.github/workflows/_static-checks.yml
integration-tests:
uses: ./.github/workflows/_integration-tests.yml
secrets:
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
build-package:
uses: ./.github/workflows/_build-package.yml
upload-package:
Expand Down
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
[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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: GNU Affero General Public License v3",
]
authors = [{ name = "Szabolcs Antal", email = "[email protected]" }]

[project.optional-dependencies]
audit = ["bandit", "pip-audit"]
build = ["build", "twine"]
lint = ["black", "flake8", "flake8-pyproject", "mypy", "pre-commit"]
lint = ["black", "flake8-pyproject", "mypy", "pre-commit"]
test = ["pytest", "pytest-cov"]
dev = ["thehive4py[audit, lint, test, build]"]

Expand Down
10 changes: 5 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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="[email protected]",
password="secret",
admin_org="admin",
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions tests/test_case_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion tests/test_custom_field_endpoint.py
Original file line number Diff line number Diff line change
@@ -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={
Expand Down
1 change: 1 addition & 0 deletions tests/test_user_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
60 changes: 51 additions & 9 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import requests

from thehive4py.client import TheHiveApi
from thehive4py.helpers import now_to_ts
from thehive4py.query.filters import Eq


Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Loading