Files
algo/tests
9268a78028 Add end-to-end VPN connectivity tests using network namespaces (#14914)
* Add end-to-end VPN connectivity tests using network namespaces

Addresses #14912

Current integration tests verify that VPN services start, but don't verify
they actually work. This adds true E2E tests using Linux network namespaces
to simulate a client connecting to the server.

New tests verify:
- WireGuard handshake completes and tunnel is functional
- IPsec/StrongSwan service is configured and listening
- DNS resolution works through VPN (172.16.0.1)
- mobileconfig XML files are valid
- CA certificate chain is correct

Changes:
- Add tests/e2e/test-vpn-connectivity.sh - main E2E test script
- Add tests/e2e/README.md - documentation for running tests
- Update integration-tests.yml to run E2E tests after deployment
- Delete tests/legacy-lxd/ - replaced by new E2E tests
- Update .ansible-lint to remove legacy-lxd from excludes
- Rewrite tests/README.md for clarity

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix WireGuard handshake timeout by allowing VPN traffic on veth

The namespace test was timing out because the firewall was blocking
UDP traffic on the veth interface. This adds explicit INPUT rules
to allow WireGuard (51820) and IPsec (500, 4500) traffic.

Also refines the MASQUERADE rule to not apply to bridge-local traffic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Use -I instead of -A for iptables rules; add debug output

The firewall rules were being appended (-A) after existing DROP rules
and never matched. Changed to -I to insert at beginning of chain.

Also added debug output to show:
- Server WireGuard peers before client connects
- Server port listening status
- iptables INPUT chain on timeout (to verify rules)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Work around deployment bug where WireGuard handlers don't fire

The async role execution in server.yml causes handlers not to fire
properly. This workaround restarts WireGuard if no peers are found,
ensuring the peer configuration is loaded.

Root cause: import_role with async: 300, poll: 0 breaks handler
notification flow. The 'restart wireguard' handler is notified but
never executed because the async context loses track of handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add packet capture and rp_filter diagnostics to debug WireGuard handshake

- Disable reverse path filtering on veth interface (can drop packets)
- Add tcpdump capture to see if UDP packets are arriving
- Show host and namespace routing tables
- Add route debugging to error output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add PersistentKeepalive to trigger WireGuard handshake

WireGuard only initiates a handshake when there's outgoing traffic or
a keepalive timer fires. Without PersistentKeepalive, the test was
waiting forever because no traffic was being sent through the tunnel
(Table=off prevents route creation).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Clean up verbose debug output from WireGuard tests

Remove routing table and rp_filter debug output that was printed on every
run. Keep the packet capture and detailed error diagnostics that are only
shown on failure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Document configuration assumptions in E2E test README

Add explicit documentation about the hardcoded IP addresses and test user
requirements as suggested in code review. This helps users understand what
default values are expected and why tests might fail on custom configurations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove unused pip cache from integration tests workflow

We use uv for dependency management, not pip, so the pip cache setting
was causing warnings about missing cache folders.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-29 02:16:33 -05:00
..

Tests

Running Tests

# Run all linters (same as CI)
ansible-lint . && yamllint . && ruff check . && shellcheck scripts/*.sh

# Run Python unit tests
pytest tests/unit/ -q

# Run E2E connectivity tests (requires deployed Algo on localhost)
sudo tests/e2e/test-vpn-connectivity.sh both

Directory Structure

tests/
├── unit/                    # Python unit tests (pytest)
│   ├── test_basic_sanity.py
│   ├── test_config_validation.py
│   ├── test_template_rendering.py
│   └── ...
├── e2e/                     # End-to-end connectivity tests
│   └── test-vpn-connectivity.sh
├── integration/             # Integration test helpers
│   └── mock_modules/
├── fixtures/                # Shared test data
│   └── test_variables.yml
└── conftest.py              # Pytest configuration

Test Coverage

Category Tests What's Verified
Sanity test_basic_sanity.py Python version, config syntax, playbook validity
Config test_config_validation.py WireGuard/IPsec config formats, key validation
Templates test_template_rendering.py Jinja2 template syntax, filter compatibility
Certificates test_certificate_validation.py OpenSSL compatibility, PKCS#12 export
Cloud Providers test_cloud_provider_configs.py Region formats, instance types, OS images
E2E test-vpn-connectivity.sh WireGuard handshake, IPsec connection, DNS through VPN

CI Workflows

Workflow Trigger What It Does
lint.yml All PRs ansible-lint, yamllint, ruff, shellcheck
main.yml Push to master Syntax check, unit tests, Docker build
integration-tests.yml PRs to roles/ Full localhost deployment + E2E tests
smart-tests.yml All PRs Runs subset based on changed files

Writing Tests

Python Unit Tests

Place in tests/unit/. Use fixtures from conftest.py:

def test_something(mock_ansible_module, jinja_env):
    # mock_ansible_module - mocked AnsibleModule
    # jinja_env - Jinja2 environment with Ansible filters
    pass

Shell Scripts

Use bash strict mode and pass shellcheck:

#!/bin/bash
set -euo pipefail

Troubleshooting

E2E tests fail with "namespace already exists"

sudo ip netns del algo-client

Template tests fail with "filter not found" Add the filter to the mock in conftest.py.

CI fails but local passes Check Python/Ansible versions match CI (Python 3.11, Ansible 12+).