Skip to content

Add comprehensive end-to-end integration tests with real Docker#65

Merged
aidankhogg merged 13 commits into
dev/alphafrom
claude/alpha-v1-roadmap-x952uw
Jun 28, 2026
Merged

Add comprehensive end-to-end integration tests with real Docker#65
aidankhogg merged 13 commits into
dev/alphafrom
claude/alpha-v1-roadmap-x952uw

Conversation

@aidankhogg

Copy link
Copy Markdown
Contributor

Summary

This PR adds two comprehensive end-to-end (E2E) integration test suites that validate the full NetEngine stack using real Docker containers, live DNS queries, ACME directory endpoints, and optional OIDC integration. These tests complement the existing unit and integration test suites by exercising real infrastructure components.

Key Changes

New Test Files

  • tests/integration/test_e2e_fullstack.py (297 lines)

    • Phase 0: Validates Docker network creation (core and platform networks)
    • Phases 1-2: Validates CoreDNS container startup and live UDP DNS SOA queries to root.internal
    • Phase 3: Validates step-ca ACME directory endpoint returns valid JSON
    • OIDC: Optional test for Keycloak token endpoint (requires NETENGINE_KEYCLOAK_URL env var)
    • Includes helper functions for Docker client management, DNS query construction, and cleanup
  • tests/integration/test_e2e_federation.py (235 lines)

    • Tests cross-world federation in PEERED mode
    • Validates GatewayPortalHandler correctly appends DNS forwarding stubs to CoreDNS Corefile
    • Verifies CoreDNS reloads configuration (SIGHUP) without crashing
    • Tests NONE mode to ensure no unintended Corefile modifications
    • Shared Docker helper functions with fullstack tests

Test Infrastructure Updates

  • tests/conftest.py: Added --run-e2e pytest option to gate E2E tests (skipped by default)

    • Implements pytest_addoption() and pytest_collection_modifyitems() hooks
    • E2E tests only run when explicitly enabled via CLI flag
  • .github/workflows/ci.yaml: New e2e job in CI pipeline

    • Runs on Python 3.13 with Ubuntu latest
    • Pre-warms CoreDNS image cache to reduce test duration
    • Runs fullstack and federation tests with --run-e2e -m "e2e and not slow"
    • Excludes slow tests (step-ca image pull, Keycloak startup) from default CI

Source Code Modifications

  • netengine/handlers/dns.py: Changed CoreDNS volume mount from read-only (ro) to read-write (rw)

    • Enables GatewayPortalHandler to append DNS stub zones at runtime
    • Necessary for federation test to modify Corefile dynamically
  • netengine/handlers/docker_handler.py: Fixed exec_run() demux parameter handling

    • Corrects output decoding for container command execution
  • pyproject.toml: Updated pytest markers

    • Added e2e marker for end-to-end tests
    • Updated slow marker description to clarify scope
  • README.md: Updated roadmap

    • Marked "End-to-end integration test" as complete

Notable Implementation Details

  1. Raw DNS Query Implementation: Tests use hand-crafted DNS SOA queries (no third-party DNS library) to validate CoreDNS responsiveness, keeping the test self-contained.

  2. Docker Cleanup: Both test files include robust cleanup logic that removes all netengine_* containers and core/platform networks, even if tests fail.

  3. SSL/TLS Handling: ACME directory test disables certificate verification for self-signed step-ca certificates using ssl.create_default_context() with CERT_NONE.

  4. Optional OIDC Test: Keycloak test gracefully skips if NETENGINE_KEYCLOAK_URL is not set, allowing the test suite to run in CI without external dependencies.

  5. Async/Await Pattern: All tests use @pytest.mark.asyncio and properly await handler execution, maintaining consistency with the async handler architecture.

  6. Marker-Based Filtering: Slow tests (step-ca image pull) are marked @pytest.mark.slow and excluded

https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh

claude added 2 commits June 28, 2026 21:05
Add end-to-end integration tests for phases 0-3 (real Docker, live
CoreDNS SOA query, step-ca ACME directory) and cross-world federation
(PEERED mode DNS stub written to CoreDNS Corefile, NONE mode no-op).

Fix two handler bugs uncovered during e2e work:
- dns.py: CoreDNS started with network_mode=none then connected to
  'core' network with static IP; volume mount changed ro→rw so
  GatewayPortalHandler can append stub zones at runtime
- docker_handler.py: exec_command used demux=True but decoded .output
  as bytes; changed to demux=False for correct bytes output

Add --run-e2e pytest flag, e2e marker, and CI e2e job (non-slow tests
only, runs on ubuntu-latest with Docker pre-installed). Mark both
roadmap items complete in README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
- Run black on test_e2e_fullstack.py and test_e2e_federation.py to fix
  linting failures
- Add daemon.json step in CI e2e job to set default-address-pools to
  192.168.128.0/18 so the runner's pre-existing networks don't overlap
  with the world spec's core subnet (10.0.0.0/24)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
@aidankhogg aidankhogg self-assigned this Jun 28, 2026
@aidankhogg aidankhogg added documentation Improvements or additions to documentation good first issue Good for newcomers help wanted Extra attention is needed labels Jun 28, 2026
@aidankhogg aidankhogg marked this pull request as ready for review June 28, 2026 21:08
claude added 11 commits June 28, 2026 21:08
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
GitHub Actions ubuntu-latest runners have 10.0.0.0/24 reserved in
Docker's IPAM (the runner host sits in the 10.x.x.x range), so any
attempt to create a Docker network with subnet 10.0.0.0/24 fails with
"Pool overlaps with other one on this address space".

Add tests/fixtures/e2e-spec.yaml — identical to minimal.yaml except
the core network uses 172.20.0.0/24 and all core-IP services use
172.20.0.x addresses. Both e2e test files now load this fixture instead
of examples/minimal.yaml. The production example is unchanged.

Also removes the non-functional daemon.json address-pool workaround
from the CI e2e job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
…ssertion

dns.py _deploy_coredns() was reading `runtime_state.dns_output` to get
the CoreDNS listen IP, but dns_output is None at deploy time (it's set
after deployment completes). Fix: read from `context.spec.dns.root.listen_ip`
which is always available.

test_e2e_fullstack: substrate_output has no top-level 'healthy' key in
the real handler (only nested per-subsystem keys). Replace the KeyError-
prone assertion with `assert "networks" in substrate_output`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
Docker v1.48 rejects net.connect() on a container that was started
with network_mode="none" (private mode): "container cannot be connected
to multiple networks with one of the networks in private (none) mode".

Replace the two-step (run with network_mode=none, then net.connect) with
a single low-level API call that attaches the container to the core network
with the static IP at creation time using create_networking_config /
create_endpoint_config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
… on failure

- Increase startup sleep from 2s to 3s to give CoreDNS more time
- After container start, wait 1s and check it hasn't already exited
- In _verify_dns_service, retry SOA query up to 6 times with 2s delays
  between attempts (12s total retry window) to handle slow-start scenarios
- On all-retries-exhausted, capture and log CoreDNS container status and
  last 80 lines of logs for CI diagnosis
Without $TTL 3600 at the top of each zone file, miekg/dns (CoreDNS's
zone parser) fails to anchor the SOA record to the zone origin and
reports 'has no SOA record for origin root.internal.' causing CoreDNS
to crash-loop. Add $TTL 3600 and explicit TTL+IN fields to every
record in all three zone file generators.
apply_internet_policy and apply_peer_routing both called copy_to_container
on the netengine_gateway container, which may not exist when running
the gateway portal handler independently (e.g., in E2E federation tests
that only provision substrate + DNS). docker.errors.NotFound was not being
wrapped as GatewayError, so it propagated uncaught through _setup_peer.

- apply_internet_policy: convert container NotFound to GatewayError
- apply_peer_routing: same wrapping, preserving temp file cleanup
- _apply_internet_policy: catch GatewayError and log a warning instead
  of aborting the whole handler — DNS forwarding can still be set up
  even without a gateway container

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
CoreDNS container (coredns/coredns:1.11.3) does not have sh in its PATH,
so _configure_peer_dns and _configure_upstream_resolver were failing with
'exec: sh: executable file not found in $PATH' when trying to append
forwarding stubs via 'sh -c echo ... >> /etc/coredns/Corefile'.

Replace the shell exec approach with a direct write to the host-mounted
Corefile (context.zone_dir/Corefile), then send kill -HUP 1 to trigger
CoreDNS config reload. This works because zone_dir is bind-mounted as
/etc/coredns/ in the container, so host writes are immediately visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
…and file reads

CoreDNS 1.11.3 runs from a scratch image with no shell utilities in PATH,
so exec+kill and exec+cat both fail. Replace them with daemon-level APIs:

- DockerHandler.signal_container(): new method using container.kill(signal=)
  which sends signals via Docker daemon without needing a kill binary
- gateway_portal_handler: switch both Corefile reload calls from
  exec_command(["kill", "-HUP", "1"]) to signal_container("HUP")
- test_e2e_federation: replace _read_corefile_from_container from
  exec_run+cat to container.get_archive+tarfile extraction, which reads
  files from any container regardless of available utilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdYtbgMhKYfk5kkw3jERWh
@aidankhogg aidankhogg merged commit 6c13086 into dev/alpha Jun 28, 2026
5 checks passed
@aidankhogg aidankhogg deleted the claude/alpha-v1-roadmap-x952uw branch June 28, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation good first issue Good for newcomers help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants