Migrate declarative configs to config/ dir and remove hardcoded paths

Replace all hardcoded /home/wall/vacuum-wall paths in lib/ with Path(__file__).resolve()
auto-discovery. Move config files from data/ to config/<subsystem>/config.json.
ACME now uses ACME_HOME env var and data/acme/ for cert storage. Systemd units
and sudoers use {{ USER_NAME }}, {{ PROJECT_DIR }}, {{ ACME_HOME }} Jinja2
template variables for install-time substitution. Remove sys.path.insert boot
strap from test files.
This commit is contained in:
2026-05-14 03:31:13 +00:00
parent 817b7c409c
commit d9797b6dac
16 changed files with 131 additions and 136 deletions
-14
View File
@@ -1,14 +0,0 @@
{
"dhcp": {
"ranges": [],
"static_leases": []
},
"dns": {
"upstreams": [
"8.8.8.8",
"1.1.1.1"
],
"domain": null,
"custom_records": []
}
}
+20 -13
View File
@@ -2,7 +2,7 @@
ACME certificate manager for Vacuum Wall. ACME certificate manager for Vacuum Wall.
Wraps acme.sh to issue, renew, and manage SSL/TLS certificates Wraps acme.sh to issue, renew, and manage SSL/TLS certificates
from Let's Encrypt (or other ACME providers). acme.sh runs as the from ACME providers such as ZeroSSL or Let's Encrypt. acme.sh runs as the
vacuum-wall system user; nginx is reloaded via a deploy hook script. vacuum-wall system user; nginx is reloaded via a deploy hook script.
""" """
@@ -16,16 +16,17 @@ from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PROJECT_DIR = Path(__file__).resolve().parent.parent
_ACME_HOME = PROJECT_DIR / "data" / "acme"
_DEPLOY_HOOK = str(PROJECT_DIR / "system" / "acme-deploy.sh")
_ACME_ENVIRON = { _ACME_ENVIRON = {
"HOME": str(Path.home()), "HOME": str(PROJECT_DIR),
"PATH": os.environ.get( "PATH": os.environ.get(
"PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" "PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
), ),
} }
PROJECT_DIR = Path("/home/wall/vacuum-wall")
_DEPLOY_HOOK = str(PROJECT_DIR / "system" / "acme-deploy.sh")
def _find_acme() -> str: def _find_acme() -> str:
"""Locate the acme.sh binary on the system. """Locate the acme.sh binary on the system.
@@ -41,7 +42,7 @@ def _find_acme() -> str:
FileNotFoundError: If acme.sh cannot be found. FileNotFoundError: If acme.sh cannot be found.
""" """
candidates = [ candidates = [
Path.home() / ".acme.sh" / "acme.sh", _ACME_HOME / "acme.sh",
Path("/usr/local/bin/acme.sh"), Path("/usr/local/bin/acme.sh"),
] ]
@@ -81,12 +82,15 @@ def _run_acme(args: list[str]) -> str:
""" """
acme_bin = _find_acme() acme_bin = _find_acme()
# Check for ACME_HOME env var (set by systemd in production)
acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
cmd: list[str] = [ cmd: list[str] = [
acme_bin, acme_bin,
"--home", "--home",
str(Path.home() / ".acme.sh"), acme_home_env,
"--config-home", "--config-home",
str(Path.home() / ".acme.sh"), acme_home_env,
*args, *args,
] ]
@@ -131,7 +135,8 @@ def set_email(email: str) -> None:
def get_email() -> str: def get_email() -> str:
"""Return the ACME contact email, or '' if none is configured.""" """Return the ACME contact email, or '' if none is configured."""
try: try:
account_conf = Path.home() / ".acme.sh" / "account.conf" acme_home = Path(os.environ.get("ACME_HOME", str(_ACME_HOME)))
account_conf = acme_home / "account.conf"
if account_conf.is_file(): if account_conf.is_file():
text = account_conf.read_text() text = account_conf.read_text()
match = re.search(r"^ACME_LEEMAIL=(.+)$", text, re.MULTILINE) match = re.search(r"^ACME_LEEMAIL=(.+)$", text, re.MULTILINE)
@@ -246,7 +251,8 @@ def list_certs() -> list[dict]:
certs: list[dict] = [] certs: list[dict] = []
entries = _parse_list_output(raw) entries = _parse_list_output(raw)
acme_home = Path.home() / ".acme.sh" acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
acme_home = Path(acme_home_env)
for entry in entries: for entry in entries:
main = entry["main_domain"] main = entry["main_domain"]
@@ -393,7 +399,8 @@ def get_cert_paths(domain: str) -> dict:
Returns: Returns:
Dict with keys 'cert', 'key', 'ca', 'fullchain' mapped to paths. Dict with keys 'cert', 'key', 'ca', 'fullchain' mapped to paths.
""" """
acme_home = str(Path.home() / ".acme.sh" / domain) acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
acme_home = str(Path(acme_home_env) / domain)
return { return {
"cert": f"{acme_home}/{domain}.cert", "cert": f"{acme_home}/{domain}.cert",
"key": f"{acme_home}/{domain}.key", "key": f"{acme_home}/{domain}.key",
@@ -484,6 +491,6 @@ def _has_auto_renew(domain: str) -> bool:
under ``~/.acme.sh/``; existence of this file means the systemd under ``~/.acme.sh/``; existence of this file means the systemd
timer's ``--cron`` run will pick it up. timer's ``--cron`` run will pick it up.
""" """
acme_home = Path.home() / ".acme.sh" acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
domain_conf = acme_home / f"{domain}.conf" domain_conf = Path(acme_home_env) / f"{domain}.conf"
return bool(domain_conf.is_file()) return bool(domain_conf.is_file())
+4 -2
View File
@@ -15,9 +15,10 @@ from typing import Any
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
PROJECT_DIR = Path("/home/wall/vacuum-wall") PROJECT_DIR = Path(__file__).resolve().parent.parent
CONFIG_DIR = PROJECT_DIR / "config" / "dnsmasq"
DATA_DIR = PROJECT_DIR / "data" / "dnsmasq" DATA_DIR = PROJECT_DIR / "data" / "dnsmasq"
CONFIG_PATH = DATA_DIR / "config.json" CONFIG_PATH = CONFIG_DIR / "config.json"
FRAGMENTS_DIR = DATA_DIR / "fragments" FRAGMENTS_DIR = DATA_DIR / "fragments"
DNSMASQ_CONF = "/etc/dnsmasq.d/vacuum-wall.conf" DNSMASQ_CONF = "/etc/dnsmasq.d/vacuum-wall.conf"
LEASE_FILE = "/var/lib/dnsmasq/dnsmasq.leases" LEASE_FILE = "/var/lib/dnsmasq/dnsmasq.leases"
@@ -46,6 +47,7 @@ DEFAULT_CFG: dict[str, Any] = {
def _ensure_dirs() -> None: def _ensure_dirs() -> None:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
FRAGMENTS_DIR.mkdir(parents=True, exist_ok=True) FRAGMENTS_DIR.mkdir(parents=True, exist_ok=True)
+9 -3
View File
@@ -12,10 +12,11 @@ from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
PROJECT_DIR = Path("/home/wall/vacuum-wall") PROJECT_DIR = Path(__file__).resolve().parent.parent
CONFIG_DIR = PROJECT_DIR / "config" / "nginx"
DATA_DIR = PROJECT_DIR / "data" / "nginx" DATA_DIR = PROJECT_DIR / "data" / "nginx"
SITES_DIR = DATA_DIR / "sites-enabled" SITES_DIR = DATA_DIR / "sites-enabled"
CONFIG_FILE = DATA_DIR / "config.json" CONFIG_FILE = CONFIG_DIR / "config.json"
INCLUDE_FILE = Path("/etc/nginx/conf.d/vacuum-wall.conf") INCLUDE_FILE = Path("/etc/nginx/conf.d/vacuum-wall.conf")
SSL_SNIPPET = Path("/etc/nginx/snippets/vacuum-wall-ssl.conf") SSL_SNIPPET = Path("/etc/nginx/snippets/vacuum-wall-ssl.conf")
HTPASSWD_FILE = DATA_DIR / ".htpasswd" HTPASSWD_FILE = DATA_DIR / ".htpasswd"
@@ -53,6 +54,7 @@ DEFAULT_CONFIG = {
def _ensure_dirs(): def _ensure_dirs():
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
SITES_DIR.mkdir(parents=True, exist_ok=True) SITES_DIR.mkdir(parents=True, exist_ok=True)
@@ -178,13 +180,15 @@ def generate_server_conf(domain_cfg: dict) -> str:
cert=domain_cfg.get("cert"), cert=domain_cfg.get("cert"),
auth=domain_cfg.get("auth"), auth=domain_cfg.get("auth"),
is_management=False, is_management=False,
acme_home=str(PROJECT_DIR / "data" / "acme"),
certs_dir=str(PROJECT_DIR / "data" / "certs"),
) )
def _generate_management_conf(management: dict) -> str: def _generate_management_conf(management: dict) -> str:
tmpl = ENV.get_template("nginx/server_block.conf") tmpl = ENV.get_template("nginx/server_block.conf")
return tmpl.render( return tmpl.render(
domain=management.get("domain", "wall.lan"), domain=management.get("domain"),
backend=dict( backend=dict(
management.get("backend", {}), host="127.0.0.1", port=9090, proto="http" management.get("backend", {}), host="127.0.0.1", port=9090, proto="http"
), ),
@@ -193,6 +197,8 @@ def _generate_management_conf(management: dict) -> str:
cert=None, cert=None,
auth=management.get("auth"), auth=management.get("auth"),
is_management=True, is_management=True,
acme_home=str(PROJECT_DIR / "data" / "acme"),
certs_dir=str(PROJECT_DIR / "data" / "certs"),
) )
+3 -3
View File
@@ -13,8 +13,8 @@ from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
PROJECT_DIR = Path("/home/wall/vacuum-wall") PROJECT_DIR = Path(__file__).resolve().parent.parent
CONFIG_PATH = str(PROJECT_DIR / "data" / "wireguard" / "config.json") CONFIG_PATH = str(PROJECT_DIR / "config" / "wireguard" / "config.json")
WG_CONF_PATH = "/etc/wireguard/wg0.conf" WG_CONF_PATH = "/etc/wireguard/wg0.conf"
WG_QUICK_BIN = "wg-quick" WG_QUICK_BIN = "wg-quick"
WG_BIN = "wg" WG_BIN = "wg"
@@ -141,7 +141,7 @@ def apply() -> None:
conf_text = generate_conf(cfg) conf_text = generate_conf(cfg)
save_config(cfg) # ensure latest state persisted save_config(cfg) # ensure latest state persisted
local_dir = Path("/home/wall/vacuum-wall/data/wireguard") local_dir = PROJECT_DIR / "data" / "wireguard"
local_dir.mkdir(parents=True, exist_ok=True) local_dir.mkdir(parents=True, exist_ok=True)
local_tmp = local_dir / "wg0.conf.tmp" local_tmp = local_dir / "wg0.conf.tmp"
with open(local_tmp, "w") as f: with open(local_tmp, "w") as f:
+6 -6
View File
@@ -21,21 +21,21 @@ server {
{% if cert.type == "acme" %} {% if cert.type == "acme" %}
# Certificate managed by acme.sh # Certificate managed by acme.sh
{% if cert.email %} # ACME contact: {{ cert.email }} {% if cert.email %} # ACME contact: {{ cert.email }}
{% endif %} ssl_certificate /home/vacuum-wall/.acme.sh/{{ domain }}/fullchain.cer; {% endif %} ssl_certificate {{ acme_home }}/{{ domain }}/fullchain.cer;
ssl_certificate_key /home/vacuum-wall/.acme.sh/{{ domain }}/{{ domain }}.key; ssl_certificate_key {{ acme_home }}/{{ domain }}/{{ domain }}.key;
{% elif cert.type == "file" %} {% elif cert.type == "file" %}
ssl_certificate {{ cert.path }}; ssl_certificate {{ cert.path }};
ssl_certificate_key {{ cert.key_path }}; ssl_certificate_key {{ cert.key_path }};
{% elif cert.type == "selfsigned" %} {% elif cert.type == "selfsigned" %}
ssl_certificate /home/wall/vacuum-wall/data/certs/{{ domain }}.crt; ssl_certificate {{ certs_dir }}/{{ domain }}.crt;
ssl_certificate_key /home/wall/vacuum-wall/data/certs/{{ domain }}.key; ssl_certificate_key {{ certs_dir }}/{{ domain }}.key;
{% endif %} {% endif %}
{% elif is_management %} {% elif is_management %}
ssl_certificate /home/wall/vacuum-wall/data/certs/{{ domain }}.crt; ssl_certificate {{ certs_dir }}/{{ domain }}.crt;
ssl_certificate_key /home/wall/vacuum-wall/data/certs/{{ domain }}.key; ssl_certificate_key {{ certs_dir }}/{{ domain }}.key;
{% endif %} {% endif %}
# Shared SSL settings # Shared SSL settings
+22 -22
View File
@@ -1,33 +1,33 @@
# Defaults directives # Defaults directives
Defaults:vacuum-wall !requiretty Defaults:{{ USER_NAME }} !requiretty
Defaults:vacuum-wall secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" Defaults:{{ USER_NAME }} secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Firewall management # Firewall management
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/firewall-cmd * {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/firewall-cmd *
# Nginx management # Nginx management
vacuum-wall ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload
vacuum-wall ALL=(root) NOPASSWD: /usr/sbin/nginx -t {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -t
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/ {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/conf.d/ {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/conf.d/
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/snippets/ {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/snippets/
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/conf.d/vacuum-wall.conf {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/conf.d/vacuum-wall.conf
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/snippets/vacuum-wall-ssl.conf {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/snippets/vacuum-wall-ssl.conf
# Dnsmasq management # Dnsmasq management
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/systemctl reload dnsmasq {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl reload dnsmasq
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/systemctl is-active dnsmasq {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl is-active dnsmasq
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/dnsmasq.d/ {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/dnsmasq.d/
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/dnsmasq/dnsmasq.leases {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/dnsmasq/dnsmasq.leases
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/tee /etc/dnsmasq.d/vacuum-wall.conf {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/tee /etc/dnsmasq.d/vacuum-wall.conf
# WireGuard management # WireGuard management
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/wg-quick * {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg-quick *
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/wg * {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg *
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/wireguard/ {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/wireguard/
# Misc # Misc
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n * {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/* {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/dnsmasq.d {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/dnsmasq.d
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/wireguard {{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/wireguard
+5 -3
View File
@@ -3,6 +3,8 @@ Description=Vacuum Wall ACME Certificate Renewal
[Service] [Service]
Type=oneshot Type=oneshot
User=vacuum-wall User={{ USER_NAME }}
WorkingDirectory=/home/wall/vacuum-wall WorkingDirectory={{ PROJECT_DIR }}
ExecStart=/usr/local/bin/acme.sh --cron --home /home/vacuum-wall/.acme.sh Environment=ACME_HOME={{ PROJECT_DIR }}/data/acme
Environment=HOME={{ PROJECT_DIR }}
ExecStart=/usr/local/bin/acme.sh --cron --home {{ ACME_HOME }}
+9 -7
View File
@@ -6,20 +6,21 @@ Wants=firewalld.service
[Service] [Service]
Type=simple Type=simple
User=vacuum-wall User={{ USER_NAME }}
Group=vacuum-wall Group={{ USER_NAME }}
WorkingDirectory=/home/wall/vacuum-wall WorkingDirectory={{ PROJECT_DIR }}
ExecStart=/home/wall/vacuum-wall/.venv/bin/python webui/server.py ExecStart={{ PROJECT_DIR }}/.venv/bin/python webui/server.py
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
Environment=PATH=/usr/local/bin:/usr/bin Environment=PATH=/usr/local/bin:/usr/bin
Environment=PYTHONUNBUFFERED=1 Environment=PYTHONUNBUFFERED=1
Environment=ACME_HOME={{ PROJECT_DIR }}/data/acme
Environment=HOME={{ PROJECT_DIR }}
# Security hardening # Security hardening
NoNewPrivileges=yes NoNewPrivileges=yes
ProtectSystem=strict ProtectSystem=strict
ProtectHome=read-only ReadWritePaths={{ PROJECT_DIR }}/config {{ PROJECT_DIR }}/data /tmp
ReadWritePaths=/home/wall/vacuum-wall/data /tmp
PrivateTmp=yes PrivateTmp=yes
ProtectKernelTunables=yes ProtectKernelTunables=yes
ProtectKernelModules=yes ProtectKernelModules=yes
@@ -33,7 +34,8 @@ LockPersonality=yes
SystemCallFilter=@system-service SystemCallFilter=@system-service
PrivateDevices=yes PrivateDevices=yes
# Network - only loopback (nginx proxies to us) ProtectHome=read-only
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=all IPAddressDeny=all
IPAddressAllow=localhost IPAddressAllow=localhost
-3
View File
@@ -1,3 +0,0 @@
import sys
sys.path.insert(0, "/home/wall/vacuum-wall")
+40 -44
View File
@@ -1,4 +1,3 @@
import sys
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -6,31 +5,28 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from lib import acme from lib import acme
class TestFindAcme: class TestFindAcme:
@patch("lib.acme.shutil.which") @patch("lib.acme.shutil.which")
@patch("lib.acme.Path.home") @patch("lib.acme._ACME_HOME")
def test_finds_in_home(self, mock_home, mock_which): def test_finds_in_acme_home(self, mock_acme_home, mock_which):
mock_home.return_value = Path("/tmp/fakehome") mock_acme_home = Path("/tmp/fake-acme-home")
acme_path = mock_home.return_value / ".acme.sh" / "acme.sh" mock_acme_home.mkdir(parents=True, exist_ok=True)
acme_path.parent.mkdir(parents=True, exist_ok=True) acme_bin = mock_acme_home / "acme.sh"
acme_path.write_text("#!/bin/sh\n") acme_bin.write_text("#!/bin/sh\n")
acme_path.chmod(0o755) acme_bin.chmod(0o755)
try:
with patch.object(acme, "_ACME_HOME", mock_acme_home):
result = acme._find_acme() result = acme._find_acme()
assert "acme.sh" in result assert "acme.sh" in result
finally:
acme_path.unlink()
@patch("lib.acme.shutil.which") acme_bin.unlink()
@patch("lib.acme.Path.home")
def test_raises_when_not_found(self, mock_home, mock_which): @patch("lib.acme._find_acme")
mock_home.return_value = Path("/tmp/nonexistent-acme-dir") def test_raises_when_not_found(self, mock_find):
mock_which.return_value = None mock_find.side_effect = FileNotFoundError()
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
acme._find_acme() acme._find_acme()
@@ -112,32 +108,30 @@ class TestDaysUntil:
class TestGetEmail: class TestGetEmail:
def test_returns_empty_when_no_account_conf(self): def test_returns_empty_when_no_account_conf(self):
with patch("lib.acme.Path.home") as mock_home: with patch.object(acme, "_ACME_HOME", Path("/tmp/no-acme-email")):
mock_home.return_value = Path("/tmp/no-acme-email")
result = acme.get_email() result = acme.get_email()
assert result == "" assert result == ""
def test_parses_email_from_account_conf(self): def test_parses_email_from_account_conf(self):
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
acme_dir = Path(tmpdir) / ".acme.sh" acme_dir = Path(tmpdir) / "data" / "acme"
acme_dir.mkdir(exist_ok=True) acme_dir.mkdir(parents=True, exist_ok=True)
conf = acme_dir / "account.conf" conf = acme_dir / "account.conf"
conf.write_text("ACME_LEEMAIL='test@example.com'\n") conf.write_text("ACME_LEEMAIL='test@example.com'\n")
with patch("lib.acme.Path.home", return_value=Path(tmpdir)): with patch.object(acme, "_ACME_HOME", acme_dir):
result = acme.get_email() result = acme.get_email()
assert result == "test@example.com" assert result == "test@example.com"
class TestGetCertPaths: class TestGetCertPaths:
@patch("lib.acme.Path.home") def test_returns_paths(self, tmp_path):
def test_returns_paths(self, mock_home): with patch.object(acme, "_ACME_HOME", tmp_path / "data" / "acme"):
mock_home.return_value = Path("/home/user") paths = acme.get_cert_paths("example.com")
paths = acme.get_cert_paths("example.com") assert paths["cert"].endswith("example.com/example.com.cert")
assert paths["cert"].endswith("example.com/example.com.cert") assert paths["key"].endswith("example.com/example.com.key")
assert paths["key"].endswith("example.com/example.com.key") assert paths["ca"].endswith("example.com/ca.cer")
assert paths["ca"].endswith("example.com/ca.cer") assert paths["fullchain"].endswith("example.com/fullchain.cer")
assert paths["fullchain"].endswith("example.com/fullchain.cer")
class TestDeployHook: class TestDeployHook:
@@ -153,20 +147,22 @@ class TestDeployHook:
class TestHasAutoRenew: class TestHasAutoRenew:
@patch("lib.acme.Path.home") def test_true_when_conf_exists(self, tmp_path):
def test_true_when_conf_exists(self, mock_home): acme_dir = tmp_path / "data" / "acme"
tmpdir = tempfile.mkdtemp() acme_dir.mkdir(parents=True)
acme_dir = Path(tmpdir) / ".acme.sh"
acme_dir.mkdir()
conf = acme_dir / "example.com.conf" conf = acme_dir / "example.com.conf"
conf.touch() conf.touch()
mock_home.return_value = Path(tmpdir)
result = acme._has_auto_renew("example.com") with patch.object(acme, "_ACME_HOME", acme_dir):
assert result is True result = acme._has_auto_renew("example.com")
assert result is True
conf.unlink() conf.unlink()
@patch("lib.acme.Path.home") def test_false_when_conf_missing(self, tmp_path):
def test_false_when_conf_missing(self, mock_home): acme_dir = tmp_path / "data" / "acme"
mock_home.return_value = Path(tempfile.mkdtemp()) acme_dir.mkdir(parents=True)
result = acme._has_auto_renew("nonexistent.com")
assert result is False with patch.object(acme, "_ACME_HOME", acme_dir):
result = acme._has_auto_renew("nonexistent.com")
assert result is False
-3
View File
@@ -1,10 +1,7 @@
import sys
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from webui.api.certs import bp as certs_bp from webui.api.certs import bp as certs_bp
from webui.api.dhcp import bp as dhcp_bp from webui.api.dhcp import bp as dhcp_bp
from webui.api.firewall import bp from webui.api.firewall import bp
+7 -4
View File
@@ -1,25 +1,28 @@
import sys
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from lib import dnsmasq from lib import dnsmasq
@pytest.fixture @pytest.fixture
def temp_data_dir(tmp_path): def temp_data_dir(tmp_path):
original_config_dir = dnsmasq.CONFIG_DIR
original = dnsmasq.DATA_DIR original = dnsmasq.DATA_DIR
original_config = dnsmasq.CONFIG_PATH original_config = dnsmasq.CONFIG_PATH
original_fragments = dnsmasq.FRAGMENTS_DIR
dnsmasq.CONFIG_DIR = tmp_path / "dnsmasq"
dnsmasq.DATA_DIR = tmp_path / "dnsmasq" dnsmasq.DATA_DIR = tmp_path / "dnsmasq"
dnsmasq.CONFIG_PATH = dnsmasq.DATA_DIR / "config.json" dnsmasq.CONFIG_PATH = dnsmasq.CONFIG_DIR / "config.json"
dnsmasq.FRAGMENTS_DIR = dnsmasq.DATA_DIR / "fragments" dnsmasq.FRAGMENTS_DIR = dnsmasq.DATA_DIR / "fragments"
dnsmasq.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
dnsmasq.DATA_DIR.mkdir(parents=True, exist_ok=True) dnsmasq.DATA_DIR.mkdir(parents=True, exist_ok=True)
dnsmasq.FRAGMENTS_DIR.mkdir(parents=True, exist_ok=True) dnsmasq.FRAGMENTS_DIR.mkdir(parents=True, exist_ok=True)
yield tmp_path yield tmp_path
dnsmasq.CONFIG_DIR = original_config_dir
dnsmasq.DATA_DIR = original dnsmasq.DATA_DIR = original
dnsmasq.CONFIG_PATH = original_config dnsmasq.CONFIG_PATH = original_config
dnsmasq.FRAGMENTS_DIR = original_fragments
class TestDeepMerge: class TestDeepMerge:
+6 -3
View File
@@ -1,11 +1,8 @@
import sys
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from lib import nginx from lib import nginx
@@ -25,7 +22,10 @@ def temp_data_dir(tmp_path):
original_htpasswd = nginx.HTPASSWD_FILE original_htpasswd = nginx.HTPASSWD_FILE
original_ssl_snippet = nginx.SSL_SNIPPET original_ssl_snippet = nginx.SSL_SNIPPET
original_include = nginx.INCLUDE_FILE original_include = nginx.INCLUDE_FILE
original_config_dir = nginx.CONFIG_DIR
original_data_dir = nginx.DATA_DIR
nginx.CONFIG_DIR = tmp_path / "nginx"
nginx.DATA_DIR = tmp_path / "nginx" nginx.DATA_DIR = tmp_path / "nginx"
nginx.SITES_DIR = tmp_path / "nginx" / "sites-enabled" nginx.SITES_DIR = tmp_path / "nginx" / "sites-enabled"
nginx.CONFIG_FILE = tmp_path / "nginx" / "config.json" nginx.CONFIG_FILE = tmp_path / "nginx" / "config.json"
@@ -33,6 +33,7 @@ def temp_data_dir(tmp_path):
nginx.SSL_SNIPPET = tmp_path / "ssl_snippet.conf" nginx.SSL_SNIPPET = tmp_path / "ssl_snippet.conf"
nginx.INCLUDE_FILE = tmp_path / "include.conf" nginx.INCLUDE_FILE = tmp_path / "include.conf"
nginx.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
nginx.DATA_DIR.mkdir(parents=True, exist_ok=True) nginx.DATA_DIR.mkdir(parents=True, exist_ok=True)
nginx.SITES_DIR.mkdir(parents=True, exist_ok=True) nginx.SITES_DIR.mkdir(parents=True, exist_ok=True)
@@ -43,6 +44,8 @@ def temp_data_dir(tmp_path):
nginx.HTPASSWD_FILE = original_htpasswd nginx.HTPASSWD_FILE = original_htpasswd
nginx.SSL_SNIPPET = original_ssl_snippet nginx.SSL_SNIPPET = original_ssl_snippet
nginx.INCLUDE_FILE = original_include nginx.INCLUDE_FILE = original_include
nginx.CONFIG_DIR = original_config_dir
nginx.DATA_DIR = original_data_dir
class TestGetConfig: class TestGetConfig:
-3
View File
@@ -1,10 +1,7 @@
import sys
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
@pytest.fixture @pytest.fixture
def client(): def client():
-3
View File
@@ -1,12 +1,9 @@
import json import json
import sys
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
sys.path.insert(0, "/home/wall/vacuum-wall")
from lib import wireguard from lib import wireguard