Files
vacuum-wall/tests/test_nginx.py
T
mteehan e2f56b8cc8 Initial commit: SSL proxy / firewall appliance
Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP,
WireGuard, and ACME certificate management.
2026-05-07 22:24:24 +00:00

247 lines
8.1 KiB
Python

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from lib import nginx
@pytest.fixture(autouse=True)
def reset_default_config():
"""Reset the shared mutable DEFAULT_CONFIG before each test."""
original = nginx.DEFAULT_CONFIG.copy()
yield
# Reset the shared "domains" dict that leaks due to shallow copy in _json_load
nginx.DEFAULT_CONFIG = original
@pytest.fixture
def temp_data_dir(tmp_path):
original_config = nginx.CONFIG_FILE
original_sites = nginx.SITES_DIR
original_htpasswd = nginx.HTPASSWD_FILE
original_ssl_snippet = nginx.SSL_SNIPPET
original_include = nginx.INCLUDE_FILE
nginx.DATA_DIR = tmp_path / "nginx"
nginx.SITES_DIR = tmp_path / "nginx" / "sites-enabled"
nginx.CONFIG_FILE = tmp_path / "nginx" / "config.json"
nginx.HTPASSWD_FILE = tmp_path / "nginx" / ".htpasswd"
nginx.SSL_SNIPPET = tmp_path / "ssl_snippet.conf"
nginx.INCLUDE_FILE = tmp_path / "include.conf"
nginx.DATA_DIR.mkdir(parents=True, exist_ok=True)
nginx.SITES_DIR.mkdir(parents=True, exist_ok=True)
yield tmp_path
nginx.CONFIG_FILE = original_config
nginx.SITES_DIR = original_sites
nginx.HTPASSWD_FILE = original_htpasswd
nginx.SSL_SNIPPET = original_ssl_snippet
nginx.INCLUDE_FILE = original_include
class TestGetConfig:
def test_returns_default_when_no_file(self, temp_data_dir):
cfg = nginx.get_config()
assert "domains" in cfg
assert "ssl" in cfg
assert cfg["domains"] == {}
class TestSaveConfig:
def test_saves_and_reloads(self, temp_data_dir):
cfg = {
"domains": {"example.com": {"backend": {"host": "localhost", "port": 80}}}
}
nginx.save_config(cfg)
loaded = nginx.get_config()
assert loaded["domains"]["example.com"]["backend"]["host"] == "localhost"
class TestGetDomains:
def test_empty_domains(self, temp_data_dir):
result = nginx.get_domains()
assert result == []
def test_returns_domain_list(self, temp_data_dir):
cfg = {
"domains": {
"example.com": {
"backend": {"host": "localhost", "port": 8080, "proto": "http"},
"force_ssl": True,
}
}
}
nginx.save_config(cfg)
result = nginx.get_domains()
assert len(result) == 1
assert result[0]["domain"] == "example.com"
class TestAddDomain:
@patch("lib.nginx.get_config")
def test_add_domain(self, mock_get, temp_data_dir):
mock_get.return_value = {
"domains": {},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
nginx.add_domain("example.com", "10.0.0.5", 8080)
cfg = nginx.get_config()
assert "example.com" in cfg["domains"]
assert cfg["domains"]["example.com"]["backend"]["host"] == "10.0.0.5"
assert cfg["domains"]["example.com"]["backend"]["port"] == 8080
@patch("lib.nginx.get_config")
def test_duplicate_domain_raises(self, mock_get, temp_data_dir):
mock_get.return_value = {
"domains": {
"example.com": {"backend": {"host": "x", "port": 80, "proto": "http"}}
},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
with pytest.raises(ValueError):
nginx.add_domain("example.com", "10.0.0.5", 8080)
class TestRemoveDomain:
@patch("lib.nginx.get_config")
def test_remove_existing_domain(self, mock_get, temp_data_dir):
mock_get.return_value = {
"domains": {
"example.com": {"backend": {"host": "x", "port": 80, "proto": "http"}}
},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
nginx.remove_domain("example.com")
cfg = nginx.get_config()
assert "example.com" not in cfg["domains"]
def test_remove_nonexistent_domain(self, temp_data_dir):
nginx.remove_domain("nonexistent.com")
cfg = nginx.get_config()
assert "nonexistent.com" not in cfg["domains"]
class TestUpdateDomain:
@patch("lib.nginx.get_config")
def test_update_existing_domain(self, mock_get, temp_data_dir):
entry = {
"backend": {"host": "10.0.0.5", "port": 8080, "proto": "http"},
"force_ssl": True,
}
mock_get.return_value = {
"domains": {"example.com": entry},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
nginx.update_domain("example.com", force_ssl=False)
cfg = nginx.get_config()
assert cfg["domains"]["example.com"]["force_ssl"] is False
def test_update_nonexistent_raises(self, temp_data_dir):
with pytest.raises(KeyError):
nginx.update_domain("nonexistent.com", force_ssl=False)
class TestWriteSite:
def test_write_creates_file(self, temp_data_dir):
nginx.write_site("example.com", "server { listen 443; }")
path = nginx.SITES_DIR / "example.com.conf"
assert path.exists()
content = Path(path).read_text()
assert "server { listen 443; }" in content
class TestWriteAllSites:
@patch("lib.nginx.get_config")
def test_writes_all_domains(self, mock_get, temp_data_dir):
mock_get.return_value = {
"domains": {
"a.com": {
"backend": {"host": "10.0.0.1", "port": 80, "proto": "http"},
"force_ssl": True,
},
"b.com": {
"backend": {"host": "10.0.0.2", "port": 80, "proto": "http"},
"force_ssl": True,
},
},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
nginx.write_all_sites()
assert (nginx.SITES_DIR / "a.com.conf").exists()
assert (nginx.SITES_DIR / "b.com.conf").exists()
@patch("lib.nginx.get_config")
def test_removes_old_sites(self, mock_get, temp_data_dir):
# Pre-create an old site
nginx.write_site("old.com", "server {}")
assert (nginx.SITES_DIR / "old.com.conf").exists()
mock_get.return_value = {
"domains": {},
"management": None,
"ssl": {"protocols": "TLSv1.2 TLSv1.3"},
}
nginx.write_all_sites()
assert not (nginx.SITES_DIR / "old.com.conf").exists()
class TestTestConfig:
@patch("lib.nginx._run")
def test_passes(self, mock_run, temp_data_dir):
mock_run.return_value = MagicMock(
returncode=0, stdout="", stderr="test passed\n"
)
ok, _msg = nginx.test_config()
assert ok is True
@patch("lib.nginx._run")
def test_fails(self, mock_run, temp_data_dir):
mock_run.return_value = MagicMock(
returncode=1, stdout="", stderr="nginx: configuration test failed\n"
)
ok, _msg = nginx.test_config()
assert ok is False
class TestWriteHtpasswd:
@patch("lib.nginx._hash_password")
def test_creates_file(self, mock_hash, temp_data_dir):
mock_hash.return_value = "$apr1$hash"
nginx.write_htpasswd("admin", "secret")
assert nginx.HTPASSWD_FILE.exists()
content = nginx.HTPASSWD_FILE.read_text()
assert "admin:" in content
@patch("lib.nginx._hash_password")
def test_replaces_existing_user(self, mock_hash, temp_data_dir):
mock_hash.return_value = "$apr1$hash1"
nginx.write_htpasswd("admin", "old")
mock_hash.return_value = "$apr1$hash2"
nginx.write_htpasswd("admin", "new")
lines = [
line
for line in nginx.HTPASSWD_FILE.read_text().strip().splitlines()
if line
]
assert len([line for line in lines if line.startswith("admin:")]) == 1
class TestHashPasswordFallback:
@patch("lib.nginx._hash_password")
def test_hash_returns_string(self, mock_hash, temp_data_dir):
mock_hash.return_value = "$apr1$hash"
result = nginx._hash_password("test")
assert isinstance(result, str)
assert len(result) > 0