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") } }