Two main test surfaces ship in this repo:
- Framework unit tests — mock-based, no Neo4j required.
- Neo4j fixture tests — load Cypher fixtures into a real Neo4j. They are selected by the
@pytest.mark.neo4jmarker and may live anywhere undertests/.
Both suites run on every PR via CI. Framework tests run across the Python 3.9/3.13 matrix; Neo4j fixture tests run on 3.13 only against a neo4j:4.4 service container, matching the Neo4j version BloodHound CE ships.
pip install -e ".[test]"
pytest tests/ -m "not neo4j and not integration" -vThis runs every framework test and skips anything Neo4j-backed. CI runs the same selector on Python 3.9 and 3.13.
The fixture suite wipes the target Neo4j database before and after every test. Two guards prevent this from hitting a real BloodHound instance:
ADPF_TEST_ALLOW_WIPE=1must be set explicitly.- The default
NEO4J_URIisbolt://localhost:17687, not the BloodHound-standard7687. Even withADPF_TEST_ALLOW_WIPE=1, the harness refuses to wipelocalhost:7687unlessGITHUB_ACTIONS=trueorADPF_TEST_FORCE_DEFAULT_PORT=1is set.
Start a disposable container:
docker run -d --name adpf-test-neo4j \
-p 17474:7474 -p 17687:7687 \
-e NEO4J_AUTH=neo4j/testpassword \
neo4j:4.4
# Wait for Bolt:
until docker exec adpf-test-neo4j cypher-shell -u neo4j -p testpassword "RETURN 1" >/dev/null 2>&1; do sleep 2; doneRun the suite:
ADPF_TEST_ALLOW_WIPE=1 \
NEO4J_URI=bolt://localhost:17687 \
NEO4J_USER=neo4j NEO4J_PASSWORD=testpassword \
pytest tests/ -m "neo4j and not integration" -vTear down:
docker rm -f adpf-test-neo4j- Create
tests/fixtures/<your_check>.cypherwith a minimal graph that triggers your check. Look attests/fixtures/esc3_enrollment_agent.cypherfor the pattern. - Create
tests/checks/test_<your_check>.pystarting withimport pytestandpytestmark = pytest.mark.neo4jat module top, then load the fixture, run the check viarun_check(YourCheck, clean_neo4j), and assert the findings. The marker is mandatory — without it the test runs in the fast lane and trips the wipe guard. Non-check Neo4j tests may live elsewhere undertests/, but they must use the same marker. - Optionally add a negative fixture (
<your_check>_negative.cypher) and assert the check stays silent. - Run locally as shown above.
All 6 cross-domain checks under checks/cross_domain/ are covered: 3 NTDS-only checks run without Neo4j, and 3 Neo4j checks (domain_trusts, cross_domain_escalation_path, cross_domain_computer_escalation_path) use fixtures. The inventory test verifies coverage for both CheckRegistry and CrossDomainRegistry.