"""Port links behind a reverse proxy: the Docker host's address, not the domain.""" from __future__ import annotations import pytest ROUTE = """Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT eth0\t00000000\t01060A0A\t0003\t0\t0\t0\t00000000\t0\t0\t0 eth0\t00060A0A\t00000000\t0001\t0\t0\t0\tE0FFFFFF\t0\t0\t0 docker0\t000011AC\t00000000\t0001\t0\t0\t0\t0000FFFF\t0\t0\t0 """ # Trimmed from a real host: lo, eth0 10.10.6.4/27, docker0 172.17.0.1/16. FIB_TRIE = """Main: +-- 0.0.0.0/0 3 0 5 |-- 0.0.0.0 /0 universe UNICAST +-- 10.10.6.0/27 2 1 2 |-- 10.10.6.0 /27 link UNICAST |-- 10.10.6.4 /32 host LOCAL |-- 10.10.6.31 /32 link BROADCAST +-- 127.0.0.0/8 2 0 2 |-- 127.0.0.1 /32 host LOCAL +-- 172.17.0.0/16 2 0 2 |-- 172.17.0.1 /32 host LOCAL """ @pytest.fixture def host_proc(tmp_path, monkeypatch): from config import settings net = tmp_path / "1" / "net" net.mkdir(parents=True) monkeypatch.setattr(settings, "HOST_PROC_PATH", str(tmp_path)) return net def test_the_default_route_interface_address_is_detected(host_proc): from services import host_address_service (host_proc / "route").write_text(ROUTE) (host_proc / "fib_trie").write_text(FIB_TRIE) assert host_address_service.detect_host_ip() == "10.10.6.4" def test_the_lowest_metric_default_route_wins(host_proc): from services import host_address_service # A second default via docker0 with a worse metric must not take over. route = ROUTE + "docker0\t00000000\t010011AC\t0003\t0\t0\t500\t00000000\t0\t0\t0\n" (host_proc / "route").write_text(route) (host_proc / "fib_trie").write_text(FIB_TRIE) assert host_address_service.detect_host_ip() == "10.10.6.4" def test_no_host_proc_means_no_guess(tmp_path, monkeypatch): """Without the host's /proc, /proc/net is the container's own namespace; its bridge address is unreachable, so nothing is better than that.""" from config import settings from services import host_address_service monkeypatch.setattr(settings, "HOST_PROC_PATH", str(tmp_path)) assert host_address_service.detect_host_ip() is None def test_no_default_route_means_no_guess(host_proc): from services import host_address_service (host_proc / "route").write_text(ROUTE.splitlines()[0] + "\n" + ROUTE.splitlines()[2] + "\n") (host_proc / "fib_trie").write_text(FIB_TRIE) assert host_address_service.detect_host_ip() is None def test_settings_report_the_detected_address(as_user, host_proc): (host_proc / "route").write_text(ROUTE) (host_proc / "fib_trie").write_text(FIB_TRIE) r = as_user.get("/api/settings") assert r.status_code == 200 assert r.json()["detected_host_ip"] == "10.10.6.4" @pytest.mark.parametrize( "raw, stored", [ (" 10.10.6.4 ", "10.10.6.4"), ("Docker-SRV001.lan", "docker-srv001.lan"), ("[fd00::1]", "fd00::1"), ("", ""), ], ) def test_port_link_host_is_stored_bare(as_admin, raw, stored): r = as_admin.put("/api/settings", json={"port_link_host": raw}) assert r.status_code == 200, r.text assert r.json()["port_link_host"] == stored assert as_admin.get("/api/settings").json()["port_link_host"] == stored @pytest.mark.parametrize( "raw", ["http://10.10.6.4", "10.10.6.4:8080", "host/path", "two words", "-bad.lan"] ) def test_port_link_host_rejects_urls_and_junk(as_admin, raw): before = as_admin.get("/api/settings").json() r = as_admin.put( "/api/settings", json={"port_link_host": raw, "update_check_interval_minutes": before["update_check_interval_minutes"] + 7}, ) assert r.status_code == 400 after = as_admin.get("/api/settings").json() # Nothing of a rejected request is written, the interval included. assert after["update_check_interval_minutes"] == before["update_check_interval_minutes"] assert after["port_link_host"] == before["port_link_host"] def test_only_admins_set_the_port_link_host(as_user): assert as_user.put("/api/settings", json={"port_link_host": "10.0.0.1"}).status_code == 403