7013a5fd2e
Leg A: „Hálózat" card on Beállítások → Rendszer — Helyi cím (LAN), Hálózati név (only while Megosztás is enabled), Átjáró; live per render, stored nowhere (S-5), „—" on unavailable. Leg B: network section in the Debug system dump (interfaces/route/DNS/ lan_address), best-effort per item via the samba-netns door. Leg C: NetBIOS trap named — Szerver field helper text + a purely lexical hint on unreachable failures for single-label non-IP names. Design note: all guest-net reads go through docker exec into the host-networked felhom-samba container (stacks/guestnet.go, one seam) — the controller's own netns is the docker bridge, so /proc/net/route etc. would answer 172.x (the S-2 trap). Red-proofs: A2 gate-drop and C2 lexical-invert both failed as required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuFPHmHNrCJj1VhY6QdDMU
190 lines
7.2 KiB
Go
190 lines
7.2 KiB
Go
package web
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||
)
|
||
|
||
// R-66 Leg A — the „Hálózat" card on Beállítások → Rendszer.
|
||
|
||
// A1: sharing enabled → the card renders address + network-name + gateway rows with live values.
|
||
func TestNetworkCardRendersAllRows(t *testing.T) {
|
||
s := testPageServer(t)
|
||
if err := s.settings.SetSMBEnabled(true); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s.sambaAddrFn = func() string { return "192.168.0.104" }
|
||
s.guestGatewayFn = func() string { return "192.168.0.1" }
|
||
|
||
body := getPage(t, s, "/settings").Body.String()
|
||
for _, m := range []string{"Hálózat", "Helyi cím (LAN)", "192.168.0.104", "Hálózati név", `\\FELHOM`, "Átjáró", "192.168.0.1"} {
|
||
if !strings.Contains(body, m) {
|
||
t.Errorf("card missing %q", m)
|
||
}
|
||
}
|
||
// The footer sentence (remote-troubleshooting read-aloud hint) belongs to the card.
|
||
if !strings.Contains(body, "helyi hálózattól függenek") {
|
||
t.Error("card missing the footer sentence")
|
||
}
|
||
}
|
||
|
||
// A2: sharing DISABLED → the Hálózati név row is ABSENT. The WRONG case this pins: rendering
|
||
// \\FELHOM while samba is down would promise a name that does not exist on the network.
|
||
// Red-proof: drop the smb.Enabled gate in systemPageData → this fails with \\FELHOM present.
|
||
func TestNetworkCardNoSMBNameWhenSharingOff(t *testing.T) {
|
||
s := testPageServer(t)
|
||
if err := s.settings.SetSMBEnabled(false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s.sambaAddrFn = func() string { return "" }
|
||
s.guestGatewayFn = func() string { return "" }
|
||
|
||
body := getPage(t, s, "/settings").Body.String()
|
||
if !strings.Contains(body, "Hálózat") {
|
||
t.Fatal("card absent entirely")
|
||
}
|
||
if strings.Contains(body, "Hálózati név") || strings.Contains(body, `\\FELHOM`) {
|
||
t.Error("Hálózati név row rendered while Megosztás is disabled — a wrong promise")
|
||
}
|
||
}
|
||
|
||
// A3: the address helper returns "" → the row renders „—" + the muted note, no crash, and no
|
||
// fallback value appears from anywhere (nothing is stored to fall back TO — S-5).
|
||
func TestNetworkCardDashOnUnavailable(t *testing.T) {
|
||
s := testPageServer(t)
|
||
s.sambaAddrFn = func() string { return "" }
|
||
s.guestGatewayFn = func() string { return "" }
|
||
|
||
rec := getPage(t, s, "/settings")
|
||
if rec.Code != 200 {
|
||
t.Fatalf("GET /settings = %d, want 200", rec.Code)
|
||
}
|
||
body := rec.Body.String()
|
||
if !strings.Contains(body, "nem állapítható meg") {
|
||
t.Error("missing the muted unavailable note")
|
||
}
|
||
// No plausible-but-stale address may surface: the only IPs on the page must be the ones other
|
||
// cards legitimately carry — assert the card region itself carries the dash.
|
||
cardStart := strings.Index(body, "Helyi cím (LAN)")
|
||
if cardStart < 0 {
|
||
t.Fatal("card row missing")
|
||
}
|
||
region := body[cardStart:]
|
||
if end := strings.Index(region, "Átjáró"); end > 0 {
|
||
region = region[:end]
|
||
}
|
||
if !strings.Contains(region, "—") {
|
||
t.Error("Helyi cím row lacks the em-dash placeholder")
|
||
}
|
||
}
|
||
|
||
// R-66 Leg A seam freshness: the card must re-derive per render (never memoize a DHCP lease) —
|
||
// the same counted-fn assertion that guards the Megosztás connect card.
|
||
func TestNetworkCardFreshPerRender(t *testing.T) {
|
||
s := testPageServer(t)
|
||
addrCalls, gwCalls := 0, 0
|
||
s.sambaAddrFn = func() string { addrCalls++; return "192.168.0.104" }
|
||
s.guestGatewayFn = func() string { gwCalls++; return "192.168.0.1" }
|
||
|
||
getPage(t, s, "/settings")
|
||
getPage(t, s, "/settings")
|
||
if addrCalls != 2 || gwCalls != 2 {
|
||
t.Errorf("stale-value risk: addr resolved %d×, gateway %d× over 2 renders (want 2/2)", addrCalls, gwCalls)
|
||
}
|
||
}
|
||
|
||
// R-66 Leg B (web half) — the Debug dump's network section.
|
||
// B1: the dump contains `network` with the four sub-keys; a fabricated resolv.conf failure yields
|
||
// an error string IN PLACE while the dump stays complete.
|
||
func TestDebugDumpNetworkSection(t *testing.T) {
|
||
s := testPageServer(t)
|
||
s.guestNetFn = func() stacks.GuestNetSnapshot {
|
||
return stacks.GuestNetSnapshot{
|
||
Interfaces: []stacks.GuestInterface{{Name: "eth0", Up: true, Addresses: []string{"192.168.0.104/24"}}},
|
||
Gateway: "192.168.0.1",
|
||
RouteInterface: "eth0",
|
||
LANAddress: "192.168.0.104",
|
||
Errors: map[string]string{"dns": "scripted resolv.conf failure"},
|
||
}
|
||
}
|
||
|
||
rec := httptest.NewRecorder()
|
||
s.debugDump(rec, httptest.NewRequest("GET", "/api/debug/dump", nil))
|
||
if rec.Code != 200 {
|
||
t.Fatalf("dump = %d, want 200", rec.Code)
|
||
}
|
||
var dump map[string]interface{}
|
||
if err := json.Unmarshal(rec.Body.Bytes(), &dump); err != nil {
|
||
t.Fatalf("dump not JSON: %v", err)
|
||
}
|
||
network, ok := dump["network"].(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("dump lacks a network object: %T", dump["network"])
|
||
}
|
||
for _, key := range []string{"interfaces", "default_route", "dns_servers", "lan_address"} {
|
||
if _, present := network[key]; !present {
|
||
t.Errorf("network section missing %q", key)
|
||
}
|
||
}
|
||
// The failed item reports in place…
|
||
if e, _ := network["dns_servers"].(string); !strings.Contains(e, "scripted resolv.conf failure") {
|
||
t.Errorf("dns_servers = %v, want the in-place error string", network["dns_servers"])
|
||
}
|
||
// …and the healthy items are real values, not casualties.
|
||
if la, _ := network["lan_address"].(string); la != "192.168.0.104" {
|
||
t.Errorf("lan_address = %v (must equal the Hálózat card's value)", network["lan_address"])
|
||
}
|
||
route, _ := network["default_route"].(map[string]interface{})
|
||
if route["gateway"] != "192.168.0.1" || route["interface"] != "eth0" {
|
||
t.Errorf("default_route = %v", network["default_route"])
|
||
}
|
||
// The dump as a whole stayed complete (existing sections intact).
|
||
for _, key := range []string{"controller", "storage", "stacks"} {
|
||
if _, present := dump[key]; !present {
|
||
t.Errorf("dump lost its %q section", key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// R-66 Leg C — the NetBIOS trap named on an unreachable-class add failure.
|
||
func TestNetAddMessageNetBIOSHint(t *testing.T) {
|
||
const hintMark = "Windows-hálózati névnek tűnik"
|
||
|
||
// C1: single-label non-IP name + unreachable → hint present, naming the submitted value.
|
||
msg := netAddMessage("unreachable", "FELHOM", 1000)
|
||
if !strings.Contains(msg, hintMark) || !strings.Contains(msg, "»FELHOM«") {
|
||
t.Errorf("C1: hint missing from %q", msg)
|
||
}
|
||
|
||
// C2: an IP + unreachable → hint ABSENT (the wrong case: nagging an IP user about NetBIOS).
|
||
// Red-proof: invert the lexical check in looksLikeFlatNetworkName → this fails.
|
||
if msg := netAddMessage("unreachable", "192.168.0.50", 1000); strings.Contains(msg, hintMark) {
|
||
t.Errorf("C2: hint wrongly present for an IP: %q", msg)
|
||
}
|
||
|
||
// C3: dotted name → hint absent.
|
||
if msg := netAddMessage("unreachable", "nas.local", 1000); strings.Contains(msg, hintMark) {
|
||
t.Errorf("C3: hint wrongly present for a dotted name: %q", msg)
|
||
}
|
||
|
||
// The hint stays out of every OTHER category — it explains unreachability only.
|
||
if msg := netAddMessage("smb_auth", "FELHOM", 1000); strings.Contains(msg, hintMark) {
|
||
t.Errorf("hint leaked into smb_auth: %q", msg)
|
||
}
|
||
|
||
// Lexical edges: IPv6 literal (no dots, but an IP) and empty stay quiet.
|
||
if looksLikeFlatNetworkName("fe80::1") {
|
||
t.Error("IPv6 literal flagged as a NetBIOS name")
|
||
}
|
||
if looksLikeFlatNetworkName("") || looksLikeFlatNetworkName(" ") {
|
||
t.Error("empty value flagged as a NetBIOS name")
|
||
}
|
||
if !looksLikeFlatNetworkName("FELHOM") {
|
||
t.Error("FELHOM not flagged")
|
||
}
|
||
}
|