import sys import tempfile from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, "/home/wall/vacuum-wall") from lib import acme class TestFindAcme: @patch("lib.acme.shutil.which") @patch("lib.acme.Path.home") def test_finds_in_home(self, mock_home, mock_which): mock_home.return_value = Path("/tmp/fakehome") acme_path = mock_home.return_value / ".acme.sh" / "acme.sh" acme_path.parent.mkdir(parents=True, exist_ok=True) acme_path.write_text("#!/bin/sh\n") acme_path.chmod(0o755) try: result = acme._find_acme() assert "acme.sh" in result finally: acme_path.unlink() @patch("lib.acme.shutil.which") @patch("lib.acme.Path.home") def test_raises_when_not_found(self, mock_home, mock_which): mock_home.return_value = Path("/tmp/nonexistent-acme-dir") mock_which.return_value = None with pytest.raises(FileNotFoundError): acme._find_acme() class TestRunAcme: @patch("lib.acme._find_acme") @patch("lib.acme.subprocess.run") def test_success(self, mock_run, mock_find): mock_find.return_value = "/usr/local/bin/acme.sh" mock_run.return_value = MagicMock(returncode=0, stdout="success\n", stderr="") result = acme._run_acme(["--list"]) assert result == "success\n" @patch("lib.acme._find_acme") @patch("lib.acme.subprocess.run") def test_failure(self, mock_run, mock_find): mock_find.return_value = "/usr/local/bin/acme.sh" mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error\n") with pytest.raises(RuntimeError): acme._run_acme(["--list"]) class TestParseListOutput: def test_parses_single_entry(self): raw = "Main_Domain:example.com CA:LetsEncrypt Certificate_Date:2026-04-01 Certificate_Expires:2026-07-01 Certificate_Expired:No" result = acme._parse_list_output(raw) assert len(result) == 1 assert result[0]["main_domain"] == "example.com" assert result[0]["ca"] == "LetsEncrypt" def test_parses_multiple_entries(self): raw = ( "Main_Domain:a.com CA:LE Certificate_Expires:2026-07-01 Certificate_Expired:No\n" "Main_Domain:b.com CA:LE Certificate_Expires:2026-08-01 Certificate_Expired:No" ) result = acme._parse_list_output(raw) assert len(result) == 2 def test_empty_input(self): result = acme._parse_list_output("") assert result == [] def test_skips_lines_without_colons(self): raw = "some random line\nMain_Domain:a.com" result = acme._parse_list_output(raw) assert len(result) == 1 class TestDaysUntil: def test_future_date(self): from datetime import UTC, timedelta future = datetime.now(UTC) + timedelta(days=365) result = acme._days_until(future.strftime("%Y-%m-%d")) assert result >= 364 def test_empty_string(self): assert acme._days_until("") is None assert acme._days_until(None) is None def test_invalid_format(self): assert acme._days_until("not-a-date") is None def test_expired_date(self): result = acme._days_until("2020-01-01") assert result is not None assert result < 0 class TestGetEmail: def test_returns_empty_when_no_account_conf(self): with patch("lib.acme.Path.home") as mock_home: mock_home.return_value = Path("/tmp/no-acme-email") result = acme.get_email() assert result == "" def test_parses_email_from_account_conf(self): tmpdir = tempfile.mkdtemp() acme_dir = Path(tmpdir) / ".acme.sh" acme_dir.mkdir(exist_ok=True) conf = acme_dir / "account.conf" conf.write_text("ACME_LEEMAIL='test@example.com'\n") with patch("lib.acme.Path.home", return_value=Path(tmpdir)): result = acme.get_email() assert result == "test@example.com" class TestGetCertPaths: @patch("lib.acme.Path.home") def test_returns_paths(self, mock_home): mock_home.return_value = Path("/home/user") paths = acme.get_cert_paths("example.com") assert paths["cert"].endswith("example.com/example.com.cert") assert paths["key"].endswith("example.com/example.com.key") assert paths["ca"].endswith("example.com/ca.cer") assert paths["fullchain"].endswith("example.com/fullchain.cer")