* fix(docker): expose port 4096 on 0.0.0.0 for NPM proxy * test(docker): add docker-compose port exposure test * docs(handoff): set PR number for docker port expose --------- Co-authored-by: opencode-agent <agent@slaid098.dev>
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
"""Tests for docker-compose.yml port exposure.
|
|
|
|
Covers issue #50 acceptance criteria:
|
|
- ``docker-compose.yml`` exposes port 4096 on ``0.0.0.0`` (open to the network)
|
|
so that NPM (Nginx Proxy Manager) on a separate host can reach the opencode
|
|
web UI via ``193.3.168.35:4096``.
|
|
- The previous ``127.0.0.1:4096:4096`` binding (localhost-only) is gone — it
|
|
blocked cross-host NPM proxying.
|
|
|
|
PyYAML is not a direct project dependency, so the compose file is parsed
|
|
manually by tracking indentation of the ``ports:`` block (mirrors the
|
|
frontmatter parser in ``test_agent_frontmatter.py``). A raw-text assertion
|
|
is also included as a belt-and-suspenders check.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
COMPOSE_FILE = REPO_ROOT / "docker-compose.yml"
|
|
|
|
EXPECTED_BINDING = "0.0.0.0:4096:4096"
|
|
OLD_BINDING = "127.0.0.1:4096:4096"
|
|
|
|
|
|
def _compose_text() -> str:
|
|
"""Read docker-compose.yml text (asserts the file exists)."""
|
|
assert COMPOSE_FILE.exists(), f"docker-compose.yml missing: {COMPOSE_FILE}"
|
|
return COMPOSE_FILE.read_text()
|
|
|
|
|
|
def _parse_ports_bindings(text: str) -> list[str]:
|
|
"""Extract port binding strings from the ``ports:`` blocks of compose YAML.
|
|
|
|
Manual parse (no pyyaml): walk lines, detect any ``ports:`` key (it sits
|
|
under a service at indent 4), remember its indent, then collect every
|
|
following list entry at a strictly greater indent until the indent drops
|
|
back to or below the ``ports:`` indent. A list entry is a line whose
|
|
stripped form starts with ``-``.
|
|
"""
|
|
bindings: list[str] = []
|
|
in_ports = False
|
|
ports_indent = -1
|
|
for line in text.split("\n"):
|
|
if not line.strip() or line.strip().startswith("#"):
|
|
continue
|
|
stripped = line.lstrip()
|
|
indent = len(line) - len(stripped)
|
|
if stripped.startswith("ports:") and stripped.endswith(":"):
|
|
in_ports = True
|
|
ports_indent = indent
|
|
continue
|
|
if in_ports:
|
|
if indent <= ports_indent:
|
|
in_ports = False
|
|
continue
|
|
if stripped.startswith("-"):
|
|
value = stripped.lstrip("-").strip()
|
|
value = value.strip('"').strip("'")
|
|
bindings.append(value)
|
|
return bindings
|
|
|
|
|
|
# ── file exists ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_docker_compose_exists():
|
|
"""docker-compose.yml exists at repo root."""
|
|
assert COMPOSE_FILE.exists(), f"docker-compose.yml missing: {COMPOSE_FILE}"
|
|
|
|
|
|
# ── port 4096 exposed on 0.0.0.0 (issue #50 — the required binding) ──────────
|
|
|
|
|
|
def test_port_exposed_on_all_interfaces():
|
|
"""docker-compose.yml exposes 4096 on ``0.0.0.0`` (open to the network).
|
|
|
|
NPM on a separate host must reach the opencode web UI; ``0.0.0.0:4096:4096``
|
|
binds to all interfaces, ``127.0.0.1`` would block cross-host access.
|
|
"""
|
|
bindings = _parse_ports_bindings(_compose_text())
|
|
assert EXPECTED_BINDING in bindings, (
|
|
f"docker-compose.yml must bind {EXPECTED_BINDING!r} (open to network) — "
|
|
f"found port bindings: {bindings}"
|
|
)
|
|
|
|
|
|
def test_port_exposed_on_all_interfaces_raw():
|
|
"""Raw text check: the compose file contains ``0.0.0.0:4096:4096``.
|
|
|
|
Belt-and-suspenders alongside the parsed check — catches edge cases where
|
|
the manual parser might miss a quoted or unquoted binding.
|
|
"""
|
|
content = _compose_text()
|
|
assert EXPECTED_BINDING in content, (
|
|
f"docker-compose.yml must contain {EXPECTED_BINDING!r} so NPM can reach "
|
|
"the web UI from a separate host"
|
|
)
|
|
|
|
|
|
# ── old localhost-only binding is gone (issue #50 — must not regress) ───────
|
|
|
|
|
|
def test_no_localhost_only_binding():
|
|
"""docker-compose.yml does NOT bind ``127.0.0.1:4096:4096``.
|
|
|
|
The localhost-only binding blocks NPM proxying from another host; it must
|
|
not be present (and must not come back via a future regression).
|
|
"""
|
|
bindings = _parse_ports_bindings(_compose_text())
|
|
assert OLD_BINDING not in bindings, (
|
|
f"docker-compose.yml must not bind {OLD_BINDING!r} (localhost-only) — "
|
|
"NPM on a separate host cannot reach it"
|
|
)
|
|
|
|
|
|
def test_no_localhost_only_binding_raw():
|
|
"""Raw text check: ``127.0.0.1:4096:4096`` is absent from the compose file."""
|
|
content = _compose_text()
|
|
assert OLD_BINDING not in content, (
|
|
f"docker-compose.yml must not contain {OLD_BINDING!r} — the localhost-only "
|
|
"binding blocks cross-host NPM proxying"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pytest
|
|
|
|
pytest.main([__file__, "-v"])
|