mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
16 conflicts, resolved per hunk: - sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop; the single stack_host_contents write of http.conf plus vhost and the front_proxy_vhost_changed gate move into tasks/vhost.yml, with the http.conf decided per sibling domain so an onion vhost gets no HTTPS redirect. - sys-front-tls: 01_http.yml is gone as the updater intends; the cert-renew include in main.yml gates on the vhost domain's TLS. - sys-svc-compose: the updater's resolve_host_cid without the remote echo and the prepared pending flag in up.yml, written with IS_SWARM_MODE and the repeated-when block grouping. - web-app-nextcloud: the updater's post-upgrade gate and pending-only plugin configuration, which already carry the pinned-plugin handling. - web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap, the S3 blob store stays off there as TestBlobstoreRenderOrder demands. - roles_with_service: onion swap and dashboard card key combined, the canonical domain is the canonical URL's host in both branches. - sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together; run_checker forwards its options to build_docker_cmd, keeping the script under the 500-line limit. - web-app-matrix: Element branding and web_client_location side by side. - web-app-stalwart/README.md: the generated Cosmos section is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
760 lines
25 KiB
Python
760 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from utils.domains.default_primary import default_domain_primary
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
ROLE_FILES = PROJECT_ROOT / "roles/sys-ctl-hlth-csp/files/python"
|
|
sys.path.insert(0, str(ROLE_FILES))
|
|
|
|
import script # noqa: E402
|
|
|
|
|
|
class TestExtractDomainsFromFilenames(unittest.TestCase):
|
|
@patch("script.os.listdir")
|
|
def test_extract_domains_filters_valid_conf_domains(
|
|
self, mock_listdir: MagicMock
|
|
) -> None:
|
|
mock_listdir.return_value = [
|
|
"example.com.conf",
|
|
"api.example.com.conf",
|
|
"not-a-domain.txt",
|
|
"no-tld.conf",
|
|
".hidden.conf",
|
|
"a..b.com.conf",
|
|
"example.com.conf.bak",
|
|
"localhost.conf",
|
|
"sub.domain.co.uk.conf",
|
|
]
|
|
|
|
domains = script.extract_domains_from_filenames(
|
|
"/etc/nginx/conf.d/http/servers/"
|
|
)
|
|
self.assertIsInstance(domains, list)
|
|
|
|
self.assertIn("example.com", domains)
|
|
self.assertIn("api.example.com", domains)
|
|
self.assertIn("sub.domain.co.uk", domains)
|
|
|
|
self.assertNotIn("not-a-domain", domains)
|
|
self.assertNotIn("no-tld", domains)
|
|
self.assertNotIn(".hidden", domains)
|
|
self.assertNotIn("a..b.com", domains)
|
|
self.assertNotIn("example.com.conf", domains)
|
|
self.assertNotIn("localhost", domains)
|
|
|
|
@patch("script.os.listdir", side_effect=FileNotFoundError)
|
|
def test_extract_domains_returns_none_when_directory_missing(
|
|
self, _mock_listdir: MagicMock
|
|
) -> None:
|
|
domains = script.extract_domains_from_filenames("/missing")
|
|
self.assertIsNone(domains)
|
|
|
|
|
|
SUFFIX = ".onion"
|
|
DOMAIN = default_domain_primary()
|
|
|
|
|
|
def _vhosts(*domains: str) -> dict[str, Path]:
|
|
return {d: Path("/etc/nginx/servers/http") / f"{d}.conf" for d in domains}
|
|
|
|
|
|
def _argv(*extra: str) -> list[str]:
|
|
return [
|
|
"script.py",
|
|
"--nginx-config-dir",
|
|
"/etc/nginx/servers",
|
|
"--proxy-suffix",
|
|
SUFFIX,
|
|
"--image",
|
|
"img:tag",
|
|
*extra,
|
|
]
|
|
|
|
|
|
class TestCollectVhosts(unittest.TestCase):
|
|
def test_https_conf_wins_over_the_http_redirect(self) -> None:
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
for protocol, domain in (
|
|
("http", f"app.{DOMAIN}"),
|
|
("https", f"app.{DOMAIN}"),
|
|
("http", "app.abc123.onion"),
|
|
):
|
|
(Path(tmp) / protocol).mkdir(exist_ok=True)
|
|
(Path(tmp) / protocol / f"{domain}.conf").write_text("")
|
|
|
|
vhosts = script.collect_vhosts(tmp)
|
|
|
|
self.assertEqual(vhosts[f"app.{DOMAIN}"].parent.name, "https")
|
|
self.assertEqual(vhosts["app.abc123.onion"].parent.name, "http")
|
|
|
|
def test_missing_directories_return_none(self) -> None:
|
|
with patch("sys.stderr"):
|
|
self.assertIsNone(script.collect_vhosts("/definitely/missing"))
|
|
|
|
|
|
class TestSplitProxiedDomains(unittest.TestCase):
|
|
def test_splits_families(self) -> None:
|
|
domains = [
|
|
"auth.abc123.onion",
|
|
f"app.{DOMAIN}",
|
|
"matomo.abc123.onion",
|
|
]
|
|
clearnet, onion = script.split_proxied_domains(domains, SUFFIX)
|
|
self.assertEqual(clearnet, [f"app.{DOMAIN}"])
|
|
self.assertEqual(onion, ["auth.abc123.onion", "matomo.abc123.onion"])
|
|
|
|
def test_clearnet_only(self) -> None:
|
|
domains = ["a.example", "b.example"]
|
|
self.assertEqual(script.split_proxied_domains(domains, SUFFIX), (domains, []))
|
|
|
|
def test_empty_list(self) -> None:
|
|
self.assertEqual(script.split_proxied_domains([], SUFFIX), ([], []))
|
|
|
|
def test_no_suffix_proxies_nothing(self) -> None:
|
|
domains = ["a.abc123.onion"]
|
|
self.assertEqual(script.split_proxied_domains(domains, ""), (domains, []))
|
|
|
|
|
|
class TestIsSkippedDomain(unittest.TestCase):
|
|
def _skipped(self, domain: str) -> bool:
|
|
skip_set = {f"mirror.{DOMAIN}"}
|
|
labels = {d.split(".", 1)[0] for d in skip_set}
|
|
return script.is_skipped_domain(domain, skip_set, labels, SUFFIX)
|
|
|
|
def test_explicit_clearnet_domain_is_skipped(self) -> None:
|
|
self.assertTrue(self._skipped(f"mirror.{DOMAIN}"))
|
|
|
|
def test_onion_sibling_of_skipped_clearnet_is_skipped(self) -> None:
|
|
self.assertTrue(self._skipped("mirror.abc123.onion"))
|
|
|
|
def test_unrelated_onion_is_not_skipped(self) -> None:
|
|
self.assertFalse(self._skipped("auth.abc123.onion"))
|
|
|
|
def test_unrelated_clearnet_is_not_skipped(self) -> None:
|
|
self.assertFalse(self._skipped(f"auth.{DOMAIN}"))
|
|
|
|
|
|
class TestMainSkipsOnionSiblings(unittest.TestCase):
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_onion_sibling_of_skip_domain_is_excluded(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts(
|
|
f"mirror.{DOMAIN}",
|
|
"mirror.abc123.onion",
|
|
"auth.abc123.onion",
|
|
)
|
|
mock_build_urls.side_effect = lambda _vhosts, domains: [
|
|
f"http://{d}/" for d in domains
|
|
]
|
|
mock_run_checker.return_value = 0
|
|
|
|
with (
|
|
patch.object(
|
|
script.sys,
|
|
"argv",
|
|
_argv(
|
|
"--skip-domain",
|
|
f"mirror.{DOMAIN}",
|
|
"--tor-proxy",
|
|
"socks5://127.0.0.1:9050",
|
|
),
|
|
),
|
|
self.assertRaises(SystemExit),
|
|
):
|
|
script.main()
|
|
|
|
probed = [d for call in mock_build_urls.call_args_list for d in call.args[1]]
|
|
self.assertNotIn(f"mirror.{DOMAIN}", probed)
|
|
self.assertNotIn("mirror.abc123.onion", probed)
|
|
self.assertIn("auth.abc123.onion", probed)
|
|
|
|
|
|
class TestBuildDockerCmdProxy(unittest.TestCase):
|
|
def test_proxy_arg_appended(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["http://a.onion/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
proxy="socks5://127.0.0.1:9050",
|
|
)
|
|
idx = cmd.index("--proxy")
|
|
self.assertEqual(cmd[idx + 1], "socks5://127.0.0.1:9050")
|
|
|
|
def test_no_proxy_arg_without_proxy(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["https://a.example/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
proxy_bypass=".example.org",
|
|
)
|
|
self.assertNotIn("--proxy", cmd)
|
|
self.assertNotIn("--proxy-bypass", cmd)
|
|
|
|
def test_bypass_arg_follows_the_proxy(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["http://a.onion/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=["cdn.example"],
|
|
proxy="socks5://127.0.0.1:9050",
|
|
proxy_bypass=".example.org,auth.example.org",
|
|
)
|
|
idx = cmd.index("--proxy-bypass")
|
|
self.assertEqual(cmd[idx + 1], ".example.org,auth.example.org")
|
|
self.assertLess(idx, cmd.index("--"))
|
|
|
|
def test_the_allowance_list_is_closed_before_the_urls(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["http://a.onion/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
confine_to=".onion",
|
|
confine_allow=["auth.example.org"],
|
|
)
|
|
self.assertEqual(
|
|
cmd[cmd.index("--confine-to") :],
|
|
[
|
|
"--confine-to",
|
|
".onion",
|
|
"--confine-allow",
|
|
"auth.example.org",
|
|
"--",
|
|
"http://a.onion/",
|
|
],
|
|
)
|
|
|
|
|
|
class TestBuildDockerCmdTimeout(unittest.TestCase):
|
|
def test_timeout_arg_appended(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["http://a.onion/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
timeout_ms=100000,
|
|
)
|
|
idx = cmd.index("--timeout")
|
|
self.assertEqual(cmd[idx + 1], "100000")
|
|
|
|
def test_no_timeout_arg_without_one(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["https://a.example/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
)
|
|
self.assertNotIn("--timeout", cmd)
|
|
|
|
def test_the_timeout_precedes_the_url_separator(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["http://a.onion/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=["cdn.example"],
|
|
timeout_ms=100000,
|
|
)
|
|
self.assertLess(
|
|
cmd.index("--timeout"),
|
|
cmd.index("--"),
|
|
"an argument after the separator reaches the checker as a URL",
|
|
)
|
|
|
|
|
|
class TestBuildDockerCmdConcurrency(unittest.TestCase):
|
|
def test_concurrency_arg_precedes_the_url_separator(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["https://a.example/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=["cdn.example"],
|
|
concurrency=4,
|
|
)
|
|
idx = cmd.index("--concurrency")
|
|
self.assertEqual(cmd[idx + 1], "4")
|
|
self.assertLess(
|
|
idx,
|
|
cmd.index("--"),
|
|
"an argument after the separator reaches the checker as a URL",
|
|
)
|
|
|
|
def test_no_concurrency_arg_without_one(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img",
|
|
urls=["https://a.example/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
)
|
|
self.assertNotIn("--concurrency", cmd)
|
|
|
|
|
|
class TestMainTimesOutOnionsOnly(unittest.TestCase):
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_the_onion_batch_alone_carries_the_budget(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts(f"auth.{DOMAIN}", "auth.abc123.onion")
|
|
mock_build_urls.side_effect = lambda _vhosts, domains: [
|
|
f"http://{d}/" for d in domains
|
|
]
|
|
mock_run_checker.return_value = 0
|
|
|
|
with (
|
|
patch.object(
|
|
script.sys,
|
|
"argv",
|
|
_argv(
|
|
"--tor-proxy",
|
|
"socks5://127.0.0.1:9050",
|
|
"--onion-timeout",
|
|
"100000",
|
|
),
|
|
),
|
|
self.assertRaises(SystemExit),
|
|
):
|
|
script.main()
|
|
|
|
budgets = {
|
|
call.kwargs["urls"][0]: call.kwargs["timeout_ms"]
|
|
for call in mock_run_checker.call_args_list
|
|
}
|
|
self.assertEqual(budgets["http://auth.abc123.onion/"], 100000)
|
|
self.assertEqual(budgets[f"http://auth.{DOMAIN}/"], 0)
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_the_onion_batch_alone_bypasses_the_proxy_for_listed_hosts(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts(f"auth.{DOMAIN}", "auth.abc123.onion")
|
|
mock_build_urls.side_effect = lambda _vhosts, domains: [
|
|
f"http://{d}/" for d in domains
|
|
]
|
|
mock_run_checker.return_value = 0
|
|
|
|
with (
|
|
patch.object(
|
|
script.sys,
|
|
"argv",
|
|
_argv(
|
|
"--tor-proxy",
|
|
"socks5://127.0.0.1:9050",
|
|
"--tor-proxy-bypass",
|
|
f".{DOMAIN}",
|
|
"--tor-confine-allow",
|
|
f"auth.{DOMAIN}",
|
|
),
|
|
),
|
|
self.assertRaises(SystemExit),
|
|
):
|
|
script.main()
|
|
|
|
batches = {
|
|
call.kwargs["urls"][0]: call.kwargs
|
|
for call in mock_run_checker.call_args_list
|
|
}
|
|
onion = batches["http://auth.abc123.onion/"]
|
|
clearnet = batches[f"http://auth.{DOMAIN}/"]
|
|
self.assertEqual(onion["proxy_bypass"], f".{DOMAIN}")
|
|
self.assertEqual(onion["confine_to"], ".onion")
|
|
self.assertEqual(onion["confine_allow"], [f"auth.{DOMAIN}"])
|
|
self.assertEqual(clearnet["proxy_bypass"], "")
|
|
self.assertEqual(clearnet["confine_to"], "")
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_the_clearnet_batch_carries_its_own_budget(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts(f"auth.{DOMAIN}")
|
|
mock_build_urls.side_effect = lambda _vhosts, domains: [
|
|
f"http://{d}/" for d in domains
|
|
]
|
|
mock_run_checker.return_value = 0
|
|
|
|
with (
|
|
patch.object(
|
|
script.sys,
|
|
"argv",
|
|
[
|
|
"script.py",
|
|
"--nginx-config-dir",
|
|
"/etc/nginx",
|
|
"--proxy-suffix",
|
|
SUFFIX,
|
|
"--image",
|
|
"img:tag",
|
|
"--clearnet-timeout",
|
|
"45000",
|
|
],
|
|
),
|
|
self.assertRaises(SystemExit),
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(
|
|
mock_run_checker.call_args.kwargs["timeout_ms"],
|
|
45000,
|
|
"without it the clearnet batch keeps the checker's 20 s default, "
|
|
"which a heavy app outlasts and the probe reports as unreachable",
|
|
)
|
|
|
|
|
|
class TestDetectSchemeFromConf(unittest.TestCase):
|
|
def test_detects_https_via_443(self) -> None:
|
|
with patch("pathlib.Path.read_text", return_value="listen 443 ssl;"):
|
|
self.assertEqual(script.detect_scheme_from_conf(Path("x.conf")), "https")
|
|
|
|
def test_detects_https_via_ssl_flag(self) -> None:
|
|
with patch("pathlib.Path.read_text", return_value="listen 8443 ssl;"):
|
|
self.assertEqual(script.detect_scheme_from_conf(Path("x.conf")), "https")
|
|
|
|
def test_detects_http_via_80(self) -> None:
|
|
with patch("pathlib.Path.read_text", return_value="listen 80;"):
|
|
self.assertEqual(script.detect_scheme_from_conf(Path("x.conf")), "http")
|
|
|
|
def test_detects_none_when_no_listen_lines(self) -> None:
|
|
with patch("pathlib.Path.read_text", return_value="server_name example.com;"):
|
|
self.assertIsNone(script.detect_scheme_from_conf(Path("x.conf")))
|
|
|
|
def test_ignores_comments_and_blank_lines(self) -> None:
|
|
conf = """
|
|
# listen 443 ssl;
|
|
|
|
# listen 80;
|
|
server_name example.com;
|
|
"""
|
|
with patch("pathlib.Path.read_text", return_value=conf):
|
|
self.assertIsNone(script.detect_scheme_from_conf(Path("x.conf")))
|
|
|
|
def test_returns_none_when_file_missing(self) -> None:
|
|
with patch("pathlib.Path.read_text", side_effect=FileNotFoundError):
|
|
self.assertIsNone(script.detect_scheme_from_conf(Path("missing.conf")))
|
|
|
|
|
|
class TestBuildUrlsFromNginxConfs(unittest.TestCase):
|
|
@patch("script.detect_scheme_from_conf")
|
|
def test_build_urls_https_preferred(self, mock_detect: MagicMock) -> None:
|
|
mock_detect.return_value = "https"
|
|
|
|
urls = script.build_urls_from_nginx_confs(
|
|
_vhosts("example.com"), ["example.com"]
|
|
)
|
|
self.assertEqual(urls, ["https://example.com/"])
|
|
|
|
@patch("script.detect_scheme_from_conf")
|
|
def test_build_urls_http_when_http_detected(self, mock_detect: MagicMock) -> None:
|
|
mock_detect.return_value = "http"
|
|
|
|
urls = script.build_urls_from_nginx_confs(
|
|
_vhosts("example.com"), ["example.com"]
|
|
)
|
|
self.assertEqual(urls, ["http://example.com/"])
|
|
|
|
@patch("script.detect_scheme_from_conf")
|
|
def test_build_urls_falls_back_to_http_and_warns(
|
|
self, mock_detect: MagicMock
|
|
) -> None:
|
|
mock_detect.return_value = None
|
|
|
|
with patch("sys.stderr") as _stderr:
|
|
urls = script.build_urls_from_nginx_confs(
|
|
_vhosts("example.com"), ["example.com"]
|
|
)
|
|
|
|
self.assertEqual(urls, ["http://example.com/"])
|
|
|
|
|
|
class TestBuildDockerCmd(unittest.TestCase):
|
|
def test_build_docker_cmd_defaults_to_host_network_and_root_user(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="ghcr.io/kevinveenbirkenbach/csp-checker:stable",
|
|
urls=["http://example.com/"],
|
|
short_mode=False,
|
|
ignore_network_blocks_from=[],
|
|
)
|
|
|
|
self.assertEqual(cmd[0:3], ["container", "run", "--rm"])
|
|
|
|
self.assertIn("--user", cmd)
|
|
uidx = cmd.index("--user")
|
|
self.assertEqual(cmd[uidx + 1], "0:0")
|
|
|
|
self.assertIn("--network", cmd)
|
|
nidx = cmd.index("--network")
|
|
self.assertEqual(cmd[nidx + 1], "host")
|
|
|
|
self.assertIn("ghcr.io/kevinveenbirkenbach/csp-checker:stable", cmd)
|
|
self.assertEqual(cmd[-1], "http://example.com/")
|
|
|
|
def test_build_docker_cmd_can_disable_host_network(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img:tag",
|
|
urls=["http://example.com/"],
|
|
short_mode=False,
|
|
ignore_network_blocks_from=[],
|
|
use_host_network=False,
|
|
)
|
|
|
|
self.assertEqual(cmd[0:3], ["container", "run", "--rm"])
|
|
|
|
self.assertIn("--user", cmd)
|
|
uidx = cmd.index("--user")
|
|
self.assertEqual(cmd[uidx + 1], "0:0")
|
|
|
|
self.assertNotIn("--network", cmd)
|
|
self.assertNotIn("host", cmd)
|
|
|
|
def test_build_docker_cmd_short_mode(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img:tag",
|
|
urls=["http://example.com/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
)
|
|
self.assertIn("--short", cmd)
|
|
|
|
def test_build_docker_cmd_ignore_list_adds_separator_and_urls(self) -> None:
|
|
cmd = script.build_docker_cmd(
|
|
image="img:tag",
|
|
urls=["http://a.example/", "https://b.example/"],
|
|
short_mode=False,
|
|
ignore_network_blocks_from=["pxscdn.com", "cdn.example.org"],
|
|
)
|
|
|
|
self.assertIn("--ignore-network-blocks-from", cmd)
|
|
idx = cmd.index("--ignore-network-blocks-from")
|
|
|
|
self.assertEqual(cmd[idx + 1], "pxscdn.com")
|
|
self.assertEqual(cmd[idx + 2], "cdn.example.org")
|
|
self.assertEqual(cmd[idx + 3], "--")
|
|
self.assertEqual(cmd[idx + 4 :], ["http://a.example/", "https://b.example/"])
|
|
|
|
|
|
class TestRunChecker(unittest.TestCase):
|
|
@patch("script.subprocess.run")
|
|
def test_run_checker_pulls_image_when_always_pull_true(
|
|
self, mock_run: MagicMock
|
|
) -> None:
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0),
|
|
MagicMock(returncode=3),
|
|
]
|
|
|
|
rc = script.run_checker(
|
|
image="img:tag",
|
|
urls=["http://example.com/"],
|
|
short_mode=True,
|
|
ignore_network_blocks_from=[],
|
|
always_pull=True,
|
|
use_host_network=True,
|
|
)
|
|
|
|
self.assertEqual(rc, 3)
|
|
self.assertGreaterEqual(mock_run.call_count, 2)
|
|
|
|
pull_call = mock_run.call_args_list[0]
|
|
self.assertEqual(pull_call.kwargs.get("check"), False)
|
|
self.assertEqual(pull_call.args[0], ["container", "pull", "img:tag"])
|
|
|
|
run_call = mock_run.call_args_list[1]
|
|
self.assertEqual(run_call.kwargs.get("check"), False)
|
|
|
|
self.assertEqual(run_call.args[0][0:3], ["container", "run", "--rm"])
|
|
|
|
self.assertIn("--user", run_call.args[0])
|
|
uidx = run_call.args[0].index("--user")
|
|
self.assertEqual(run_call.args[0][uidx + 1], "0:0")
|
|
|
|
@patch("script.subprocess.run")
|
|
def test_run_checker_returns_127_if_run_missing(self, mock_run: MagicMock) -> None:
|
|
mock_run.side_effect = FileNotFoundError
|
|
|
|
rc = script.run_checker(
|
|
image="img:tag",
|
|
urls=["http://example.com/"],
|
|
short_mode=False,
|
|
ignore_network_blocks_from=[],
|
|
always_pull=False,
|
|
use_host_network=True,
|
|
)
|
|
self.assertEqual(rc, 127)
|
|
|
|
@patch("script.subprocess.run")
|
|
def test_run_checker_returns_1_on_unexpected_exception(
|
|
self, mock_run: MagicMock
|
|
) -> None:
|
|
mock_run.side_effect = RuntimeError("boom")
|
|
|
|
rc = script.run_checker(
|
|
image="img:tag",
|
|
urls=["http://example.com/"],
|
|
short_mode=False,
|
|
ignore_network_blocks_from=[],
|
|
always_pull=False,
|
|
use_host_network=True,
|
|
)
|
|
self.assertEqual(rc, 1)
|
|
|
|
|
|
class TestMain(unittest.TestCase):
|
|
@patch("script.run_checker")
|
|
@patch("script.collect_vhosts")
|
|
def test_main_exits_1_when_collect_vhosts_returns_none(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = None
|
|
|
|
with (
|
|
patch.object(script.sys, "argv", _argv()),
|
|
self.assertRaises(SystemExit) as cm,
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(cm.exception.code, 1)
|
|
mock_run_checker.assert_not_called()
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.collect_vhosts")
|
|
def test_main_exits_0_when_no_domains_found(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = {}
|
|
|
|
with (
|
|
patch.object(script.sys, "argv", _argv()),
|
|
self.assertRaises(SystemExit) as cm,
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(cm.exception.code, 0)
|
|
mock_run_checker.assert_not_called()
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_main_passes_defaults_and_exits_with_run_checker_rc(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts("example.com", "api.example.com")
|
|
mock_build_urls.return_value = [
|
|
"http://example.com/",
|
|
"https://api.example.com/",
|
|
]
|
|
mock_run_checker.return_value = 5
|
|
|
|
with (
|
|
patch.object(
|
|
script.sys,
|
|
"argv",
|
|
_argv(
|
|
"--short",
|
|
"--ignore-network-blocks-from",
|
|
"pxscdn.com",
|
|
"cdn.example.org",
|
|
),
|
|
),
|
|
self.assertRaises(SystemExit) as cm,
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(cm.exception.code, 5)
|
|
|
|
mock_run_checker.assert_called_once()
|
|
kwargs = mock_run_checker.call_args.kwargs
|
|
self.assertEqual(kwargs["image"], "img:tag")
|
|
self.assertEqual(
|
|
kwargs["urls"], ["http://example.com/", "https://api.example.com/"]
|
|
)
|
|
self.assertTrue(kwargs["short_mode"])
|
|
self.assertEqual(
|
|
kwargs["ignore_network_blocks_from"], ["pxscdn.com", "cdn.example.org"]
|
|
)
|
|
self.assertFalse(kwargs["always_pull"])
|
|
self.assertTrue(kwargs["use_host_network"])
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_main_no_host_network_flag_disables_host_network(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts("example.com")
|
|
mock_build_urls.return_value = ["http://example.com/"]
|
|
mock_run_checker.return_value = 0
|
|
|
|
with (
|
|
patch.object(script.sys, "argv", _argv("--no-host-network")),
|
|
self.assertRaises(SystemExit) as cm,
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(cm.exception.code, 0)
|
|
|
|
kwargs = mock_run_checker.call_args.kwargs
|
|
self.assertFalse(kwargs["use_host_network"])
|
|
|
|
@patch("script.run_checker")
|
|
@patch("script.build_urls_from_nginx_confs")
|
|
@patch("script.collect_vhosts")
|
|
def test_main_exits_0_when_no_urls_built(
|
|
self,
|
|
mock_collect: MagicMock,
|
|
mock_build_urls: MagicMock,
|
|
mock_run_checker: MagicMock,
|
|
) -> None:
|
|
mock_collect.return_value = _vhosts("example.com")
|
|
mock_build_urls.return_value = []
|
|
|
|
with (
|
|
patch.object(script.sys, "argv", _argv()),
|
|
self.assertRaises(SystemExit) as cm,
|
|
):
|
|
script.main()
|
|
|
|
self.assertEqual(cm.exception.code, 0)
|
|
mock_run_checker.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|