Skip to content
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/test-netbox-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
NETBOX_TOKEN: ${{ env.NETBOX_TOKEN }}
REPO_URL: file:///tmp/test-fixtures
REPO_BRANCH: main
run: uv run python tests/integration/test_import.py
run: uv run pytest tests/integration/ -m integration -x -v --timeout=600

- name: Print NetBox version on failure
if: failure()
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ testpaths = [
"tests"
]
norecursedirs = ["tests/integration"]
markers = [
"integration: end-to-end tests that require a live NetBox instance (set NETBOX_URL + NETBOX_TOKEN)",
]

[tool.coverage.run]
source = ["."]
Expand Down
20 changes: 16 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@


@pytest.fixture(autouse=True)
def reset_graphql_clamping_warned(mock_env_vars):
def reset_graphql_clamping_warned(mock_env_vars, request):
"""Reset the module-level page-size clamping warning dedup set before each test."""
if request.node.get_closest_marker("integration"):
yield
return
import core.graphql_client as _gql

_gql._CLAMPING_WARNED.clear()
Expand All @@ -14,8 +17,11 @@ def reset_graphql_clamping_warned(mock_env_vars):


@pytest.fixture(autouse=True)
def mock_env_vars():
def mock_env_vars(request):
"""Set mandatory environment variables to prevent settings.py from exiting."""
if request.node.get_closest_marker("integration"):
yield
return
with patch.dict(
os.environ,
{
Expand All @@ -31,8 +37,11 @@ def mock_env_vars():


@pytest.fixture(autouse=True)
def mock_git_repo():
def mock_git_repo(request):
"""Mock git.Repo to prevent actual git operations during settings import."""
if request.node.get_closest_marker("integration"):
yield None
return
with patch("core.repo.Repo") as mock_repo:
mock_remote = MagicMock()
mock_remote.url = "https://example.com/repo.git"
Expand All @@ -49,13 +58,16 @@ def mock_pynetbox():


@pytest.fixture(autouse=True)
def mock_graphql_requests():
def mock_graphql_requests(request):
"""Mock the HTTP session used by NetBoxGraphQLClient to prevent real calls.

Patches ``requests.Session`` in ``core.graphql_client`` so any client created
during a test uses a mock session. Returns empty lists for all GraphQL
list queries by default.
"""
if request.node.get_closest_marker("integration"):
yield None
return
with patch("core.graphql_client.requests.Session") as MockSession:
mock_session = MockSession.return_value
response = MagicMock()
Expand Down
Empty file added tests/integration/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Conftest for integration tests.

All tests in this package require a live NetBox instance reachable at
``NETBOX_URL`` with a valid ``NETBOX_TOKEN``. Normal ``pytest`` runs skip this
package automatically via ``norecursedirs`` in pyproject.toml; the CI workflow
invokes it explicitly with ``pytest tests/integration/ -m integration``.

When ``NETBOX_URL`` or ``NETBOX_TOKEN`` are absent every test in the package is
marked as skipped during collection, so the suite shows "s" markers instead of
errors.

Note: the env check reads ``os.environ``, which ``core.settings.load_dotenv()``
(run when the test module is imported during collection) populates from any
local ``.env``. So a developer with a configured ``.env`` will have these
tests run even with the shell vars unset; the skip path fires only in a clean
checkout (e.g. CI before its real ``NETBOX_*`` vars are exported).
"""

import os

import pytest


def pytest_collection_modifyitems(items):
"""Attach ``integration`` mark to every test in this package; skip all if env is missing."""
url = os.environ.get("NETBOX_URL", "").strip()
token = os.environ.get("NETBOX_TOKEN", "").strip()
missing_env = not url or not token
skip_marker = pytest.mark.skip(
reason=(
"Integration tests require NETBOX_URL and NETBOX_TOKEN environment variables. "
"Run: export NETBOX_URL=http://localhost:8000 NETBOX_TOKEN=<token>"
)
)
for item in items:
if "integration" in str(item.fspath):
item.add_marker(pytest.mark.integration)
if missing_env:
item.add_marker(skip_marker)
58 changes: 15 additions & 43 deletions tests/integration/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
associated data in NetBox; every subsequent test (``test_front_port_multiposition``,
``test_module_types``, ``test_graphql_schema``, ``test_idempotency``,
``test_update_mode``) depends on that initial import having completed successfully.
If an early test fails, ``sys.exit(1)`` stops the suite immediately so later tests
do not run against incomplete state. Re-running individual tests requires a fully
provisioned NetBox environment (i.e. with the test fixtures already imported).
Run the suite with ``pytest -x`` so an early failure stops it immediately and
later tests do not run against incomplete state. Re-running individual tests
requires a fully provisioned NetBox environment (i.e. with the test fixtures
already imported).

Test scenarios
--------------
Expand Down Expand Up @@ -49,7 +50,7 @@
export NETBOX_TOKEN=<token>
export REPO_URL=file:///tmp/test-fixtures # local git repo built from tests/fixtures/
export REPO_BRANCH=main
uv run python tests/integration/test_import.py
uv run pytest tests/integration/ -m integration -x -v
"""

from __future__ import annotations
Expand All @@ -61,22 +62,23 @@
from pathlib import Path
from typing import Any, NoReturn

import pytest
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))

# Import after sys.path manipulation so local modules resolve correctly.
from core.change_detector import DEVICE_TYPE_PROPERTIES # noqa: E402
from core.graphql_client import ( # noqa: E402
from core.change_detector import DEVICE_TYPE_PROPERTIES
from core.graphql_client import (
COMPONENT_TEMPLATE_FIELDS,
NetBoxGraphQLClient,
_NO_MODULE_TYPE,
)

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

pytestmark = pytest.mark.integration

REPO_ROOT = Path(__file__).resolve().parents[2]

NETBOX_URL = (os.environ.get("NETBOX_URL") or "").rstrip("/") or None
NETBOX_TOKEN = os.environ.get("NETBOX_TOKEN")
IGNORE_SSL = os.environ.get("IGNORE_SSL_ERRORS", "False").lower() == "true"
Expand All @@ -95,8 +97,7 @@

def fail(msg: str) -> NoReturn:
"""Record a failure and exit immediately."""
print(f"\n ✗ FAIL: {msg}", file=sys.stderr)
sys.exit(1)
pytest.fail(msg)


def ok(msg: str) -> None:
Expand Down Expand Up @@ -551,32 +552,3 @@ def test_update_mode() -> None:
if not re.search(r"(?<!\d)0 device types updated", result2.stdout):
fail(f"After update, third run still shows updated device types\n{result2.stdout}")
ok("Post-update run: 0 device types updated")


# ──────────────────────────────────────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────────────────────────────────────


def main() -> None:
if not NETBOX_URL or not NETBOX_TOKEN:
print(
"ERROR: NETBOX_URL and NETBOX_TOKEN environment variables are required.",
file=sys.stderr,
)
sys.exit(1)
print(f"NetBox URL : {NETBOX_URL}")
print(f"Repo root : {REPO_ROOT}")

test_first_import()
test_front_port_multiposition()
test_module_types()
test_graphql_schema()
test_idempotency()
test_update_mode()

print("\n=== All integration tests passed ✓ ===\n")


if __name__ == "__main__":
main()
Loading
Loading