firegex/tests/conftest.py
2026-09-13 15:54:56 +02:00

128 lines
5.6 KiB
Python

"""One place that knows where the instance under test is, and what may be asked of it.
Before this, four scripts each grew their own `--address`/`--password` argparse block, a
fifth read the same two things out of the environment, and a shell script held the list
of which combinations to run. The combinations are the interesting part of this suite —
the same filters on both network layers, over IPv4 and IPv6, with and without TLS — and
they belong in the test code as parameters rather than in a shell loop, where nothing can
skip one that this host cannot carry.
Selection is by command line, so a developer can narrow to the case they are fixing:
pytest # everything this host can run
pytest tests/unit # no instance needed
pytest -m "not slow" # skip the ones that wait on timeouts
pytest --layer proxy --no-ipv6 # one layer, one family
pytest --fg-address http://box:4444/ # somewhere other than localhost
"""
import os
import sys
import pytest
TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
BACKEND_DIR = os.path.abspath(os.path.join(TESTS_DIR, "..", "backend"))
LIB_DIR = os.path.abspath(os.path.join(TESTS_DIR, "..", "fgex-lib"))
# `tests/` first so `helpers` resolves here, and the backend after it. The two used to
# collide outright: this directory's package was called `utils`, which is also the name
# of the backend's, so whichever imported first won and the other module failed to
# collect. That is why these could not be run in one pytest process at all.
for path in (TESTS_DIR, LIB_DIR, BACKEND_DIR):
if path not in sys.path:
sys.path.append(path)
sys.path.insert(0, TESTS_DIR)
from helpers.firegexapi import FiregexAPI # noqa: E402
from helpers.net import supports_ipv6 # noqa: E402
def pytest_addoption(parser):
group = parser.getgroup("firegex")
group.addoption("--fg-address", action="store",
default=os.getenv("FIREGEX_ADDRESS", "http://127.0.0.1:4444/"),
help="Where the instance under test is")
group.addoption("--fg-password", action="store",
default=os.getenv("FIREGEX_PASSWORD", "testpassword"),
help="Its password")
group.addoption("--layer", action="append", default=[],
choices=["proxy", "nfqueue", "external"],
help="Only these network layers (repeatable; default: all of them)")
group.addoption("--no-ipv6", action="store_true", default=False,
help="Skip the IPv6 half of every parametrised case")
group.addoption("--no-tls", action="store_true", default=False,
help="Skip the TLS cases")
def pytest_configure(config):
for marker in (
"instance: needs a running firegex to talk to",
"root: needs root and a Linux kernel with nftables",
"slow: waits on a real timeout and takes seconds to do it",
"ipv6: exercises the IPv6 half",
"tls: exercises TLS termination",
):
config.addinivalue_line("markers", marker)
def pytest_collection_modifyitems(config, items):
"""Skip what this host or this invocation cannot carry, rather than failing it.
A skip with a reason beats a red test nobody can act on: a laptop without IPv6 on
loopback is not a firegex bug, and saying so in the summary line is more useful than
a connection error inside an assertion.
"""
wanted_layers = set(config.getoption("--layer"))
no_ipv6 = config.getoption("--no-ipv6") or not supports_ipv6()
no_ipv6_reason = ("--no-ipv6 was given" if config.getoption("--no-ipv6")
else "this host has no IPv6 loopback")
no_tls = config.getoption("--no-tls")
for item in items:
if wanted_layers:
layer = item.callspec.params.get("layer") if hasattr(item, "callspec") else None
name = getattr(layer, "transport", None)
if name and name not in wanted_layers:
item.add_marker(pytest.mark.skip(reason=f"--layer did not ask for {name}"))
if no_ipv6 and "ipv6" in item.keywords:
item.add_marker(pytest.mark.skip(reason=no_ipv6_reason))
if no_tls and "tls" in item.keywords:
item.add_marker(pytest.mark.skip(reason="--no-tls was given"))
@pytest.fixture(scope="session")
def fg_address(pytestconfig) -> str:
address = pytestconfig.getoption("--fg-address")
return address if address.endswith("/") else address + "/"
@pytest.fixture(scope="session")
def fg_password(pytestconfig) -> str:
return pytestconfig.getoption("--fg-password")
@pytest.fixture(scope="session")
def api(fg_address, fg_password) -> FiregexAPI:
"""A logged-in client, shared by everything that needs an instance.
An instance started with `--unsafe-disable-auth` answers 403 to the login endpoint
and accepts every request without a token — `login()` reads that as "there is nothing
to log in to" rather than as a failure, which is what lets the whole suite run
against the one configuration where every request is already allowed.
"""
client = FiregexAPI(fg_address)
if not client.login(fg_password):
pytest.fail(
f"Could not reach or authenticate to firegex at {fg_address}. "
f"Start one with `python3 run.py start -P {fg_password}`, or point the suite "
f"elsewhere with --fg-address."
)
return client
@pytest.fixture(scope="session")
def auth_enabled(api) -> bool:
"""Whether this instance asks for a password at all."""
return not api.status().get("auth_disabled", False)