* feat(ports): make backend/share/UI ports configurable via env vars Single-source the backend port from OMNIVOICE_PORT and derive the LAN-share base from it (OMNIVOICE_SHARE_PORT override). Previously network_share.py hardcoded BACKEND_PORT=3900, so a user running the backend on a custom port got LAN-share/Tailscale pointed at the wrong port. - network_share: replace BACKEND_PORT constant with backend_port() / share_port_base() helpers (env-driven, never-throw fallback to defaults); enable() probes from share_port_base(). - tailscale: serve_enable(port=None) defaults to network_share.backend_port(). - main.py: direct-run + --health-check ports and HEALTH_URL read OMNIVOICE_PORT; CORS default origins use OMNIVOICE_UI_PORT (default 3901). - /system/info + SystemInfoResponse: expose backend_port, share_port_base, ui_port (both success and never-throw except branches). - set-env: persist OMNIVOICE_PORT/SHARE_PORT/UI_PORT; validate numeric and 1024-65535, reject otherwise with 400. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ports): pin child OMNIVOICE_PORT in backend spawn Push OMNIVOICE_PORT=backend_port() onto the spawned Python child's env so network_share.backend_port() always agrees with the uvicorn --port Rust passes. Without this, a user-set OMNIVOICE_PORT would move the LAN-share / Tailscale target while the listener stayed on the Rust-resolved port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ports): Ports subsection in Sharing settings + env-driven Vite port - vite.config.js: dev-server port reads OMNIVOICE_UI_PORT (default 3901). - SharingPanel: new Ports subsection reads /system/info and displays backend_port / ui_port (with their env-var names + restart-to-apply note) and makes the LAN-share port editable — Save POSTs OMNIVOICE_SHARE_PORT to /system/set-env (persisted), "applies next time you enable sharing". - Tests: extend SharingPanel.test.jsx for the ports subsection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
import socket
|
|
from unittest.mock import patch
|
|
from services import network_share as ns
|
|
|
|
|
|
def _addr(ip):
|
|
class A: # mimic psutil snicaddr
|
|
family = socket.AF_INET
|
|
address = ip
|
|
return A()
|
|
|
|
|
|
def test_lan_ipv4_filters_loopback_and_linklocal():
|
|
fake = {
|
|
"lo0": [_addr("127.0.0.1")],
|
|
"en0": [_addr("192.168.1.42")],
|
|
"en1": [_addr("169.254.5.5"), _addr("10.0.0.9")],
|
|
}
|
|
with patch("services.network_share.psutil.net_if_addrs", return_value=fake):
|
|
out = ns.lan_ipv4_addresses()
|
|
assert out == ["192.168.1.42", "10.0.0.9"]
|
|
|
|
|
|
def test_gen_pin_is_six_digits():
|
|
pin = ns._gen_pin()
|
|
assert pin.isdigit() and len(pin) == 6
|
|
|
|
|
|
# ── Configurable ports (issue: user-configurable network ports) ──────────────
|
|
|
|
def test_backend_port_defaults_to_3900(monkeypatch):
|
|
monkeypatch.delenv("OMNIVOICE_PORT", raising=False)
|
|
assert ns.backend_port() == 3900
|
|
|
|
|
|
def test_backend_port_honors_env(monkeypatch):
|
|
monkeypatch.setenv("OMNIVOICE_PORT", "4000")
|
|
assert ns.backend_port() == 4000
|
|
|
|
|
|
def test_backend_port_bad_env_falls_back(monkeypatch):
|
|
monkeypatch.setenv("OMNIVOICE_PORT", "not-a-number")
|
|
assert ns.backend_port() == 3900
|
|
|
|
|
|
def test_share_port_base_defaults_to_backend_plus_one(monkeypatch):
|
|
monkeypatch.delenv("OMNIVOICE_SHARE_PORT", raising=False)
|
|
monkeypatch.setenv("OMNIVOICE_PORT", "4000")
|
|
assert ns.share_port_base() == 4001
|
|
|
|
|
|
def test_share_port_base_honors_env(monkeypatch):
|
|
monkeypatch.setenv("OMNIVOICE_SHARE_PORT", "5500")
|
|
assert ns.share_port_base() == 5500
|
|
|
|
|
|
def test_share_port_base_bad_env_falls_back(monkeypatch):
|
|
monkeypatch.delenv("OMNIVOICE_PORT", raising=False)
|
|
monkeypatch.setenv("OMNIVOICE_SHARE_PORT", "garbage")
|
|
assert ns.share_port_base() == 3901
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def _loopback_client():
|
|
from main import app
|
|
return TestClient(app, client=("127.0.0.1", 50000))
|
|
|
|
|
|
def test_network_state_endpoint_defaults_disabled():
|
|
c = _loopback_client()
|
|
r = c.get("/system/network/state")
|
|
assert r.status_code == 200
|
|
assert r.json()["enabled"] is False
|
|
|
|
|
|
def test_network_control_rejects_non_loopback():
|
|
from main import app
|
|
c = TestClient(app, client=("10.0.0.5", 9999))
|
|
assert c.post("/system/network/enable").status_code == 403
|
|
|
|
|
|
def test_system_info_has_sharing_fields():
|
|
c = _loopback_client()
|
|
body = c.get("/system/info").json()
|
|
for k in ("share_enabled", "share_port", "lan_addresses", "pin_required"):
|
|
assert k in body
|
|
|
|
|
|
def test_system_info_has_port_fields():
|
|
c = _loopback_client()
|
|
body = c.get("/system/info").json()
|
|
for k in ("backend_port", "share_port_base", "ui_port"):
|
|
assert k in body
|
|
# Defaults when no env override is set.
|
|
assert isinstance(body["backend_port"], int)
|
|
assert isinstance(body["share_port_base"], int)
|
|
assert isinstance(body["ui_port"], int)
|
|
|
|
|
|
def test_set_env_share_port_rejects_non_numeric():
|
|
c = _loopback_client()
|
|
r = c.post("/system/set-env", json={"key": "OMNIVOICE_SHARE_PORT", "value": "abc"})
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_set_env_share_port_rejects_out_of_range():
|
|
c = _loopback_client()
|
|
r = c.post("/system/set-env", json={"key": "OMNIVOICE_SHARE_PORT", "value": "80"})
|
|
assert r.status_code == 400
|
|
r = c.post("/system/set-env", json={"key": "OMNIVOICE_SHARE_PORT", "value": "70000"})
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_set_env_share_port_accepts_valid(monkeypatch):
|
|
c = _loopback_client()
|
|
r = c.post("/system/set-env", json={"key": "OMNIVOICE_SHARE_PORT", "value": "5050"})
|
|
assert r.status_code == 200
|
|
assert r.json()["set"] is True
|
|
# Clean up the process-level env mutation so other tests aren't affected.
|
|
import os
|
|
os.environ.pop("OMNIVOICE_SHARE_PORT", None)
|