Files
nixos-router/pkgs/router-webui/settings_test.go
T
nilsandClaude Opus 5.5 753459f5d7 Add a web UI for settings-configured routers
router-webui (Go, standard library only) edits settings.json: host
name, WAN (DHCP or PPPoE), LANs with DHCP, and the raw JSON for the
rest; keys the forms do not know are kept. It applies and confirms by
starting router-apply.service and router-confirm.service, which a
switch never restarts, and shows the build log and the rollback
countdown. HTTPS with a self-signed certificate, a PBKDF2 password
hash, CSRF tokens and an Origin check; reachable from management LANs
only (new router.managementPorts).

router-apply records the settings it built and router-confirm keeps
them as settings.confirmed.json (new rollback.onConfirm hook), so the
UI can tell unapplied changes and discard them.

checks.apply now also logs in from a LAN client, discards the change
left by the rollback, edits the DHCP pool, applies and confirms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:43:58 +01:00

131 lines
3.6 KiB
Go

package main
import (
"errors"
"strings"
"testing"
)
const sample = `{
"system": { "hostName": "router", "stateVersion": "26.11" },
"router": {
"wan": { "interface": "eth1" },
"lans": {
"main": {
"interface": "eth2", "internet": true,
"ipv4": { "address": "192.168.10.1" },
"dhcp": { "pool": { "start": "192.168.10.100", "end": "192.168.10.199" },
"reservations": { "nas": { "mac": "aa:bb:cc:dd:ee:ff", "address": "192.168.10.5" } } },
"ipv6": { "address": "fd00::1/64" }
}
},
"wireguard": { "vpn": { "listenPort": 51820 } }
}
}`
func mustParse(t *testing.T) Settings {
t.Helper()
s, err := parseSettings([]byte(sample))
if err != nil {
t.Fatal(err)
}
return s
}
func TestFormReadsSettings(t *testing.T) {
f := mustParse(t).form()
if f.HostName != "router" || f.WAN.Interface != "eth1" || f.WAN.Type != "dhcp" {
t.Fatalf("unexpected form: %+v", f)
}
if len(f.LANs) != 1 || f.LANs[0].Name != "main" || !f.LANs[0].DHCP || !f.LANs[0].Internet ||
f.LANs[0].PoolEnd != "192.168.10.199" {
t.Fatalf("unexpected LANs: %+v", f.LANs)
}
}
func TestApplyKeepsUnknownKeys(t *testing.T) {
s := mustParse(t)
f := s.form()
f.LANs[0].PoolEnd = "192.168.10.150"
if _, err := s.apply(f, "/var/lib/router/pppoe-password"); err != nil {
t.Fatal(err)
}
out, _ := s.marshal()
for _, want := range []string{`"192.168.10.150"`, `"reservations"`, `"fd00::1/64"`, `"wireguard"`, `"stateVersion"`} {
if !strings.Contains(string(out), want) {
t.Errorf("%s missing after apply:\n%s", want, out)
}
}
}
func TestApplyPPPoE(t *testing.T) {
s := mustParse(t)
f := s.form()
f.WAN.Type = "pppoe"
f.WAN.PPPoEUser = "user@isp"
f.WAN.PPPoEVLAN = "7"
if _, err := s.apply(f, "/pw"); err == nil {
t.Fatal("PPPoE without any password was accepted")
}
s = mustParse(t)
f.WAN.PPPoEPassword = "secret"
pw, err := s.apply(f, "/pw")
if err != nil || pw != "secret" {
t.Fatalf("apply: %q %v", pw, err)
}
if got := str(s, "router", "wan", "ipv4", "pppoe", "passwordFile"); got != "/pw" {
t.Errorf("passwordFile = %q", got)
}
out, _ := s.marshal()
if strings.Contains(string(out), "secret") {
t.Error("the password ended up in the settings")
}
if !strings.Contains(string(out), `"vlan": 7`) {
t.Errorf("vlan is not a number:\n%s", out)
}
}
func TestApplyValidates(t *testing.T) {
s := mustParse(t)
f := s.form()
f.LANs[0].Address = "not-an-ip"
f.LANs[0].VLAN = "5000"
f.LANs = append(f.LANs, LANForm{Name: "Bad Name", Interface: "eth3", Address: "10.0.0.1"})
_, err := s.apply(f, "/pw")
var fe FormErrors
if !errors.As(err, &fe) {
t.Fatalf("expected form errors, got %v", err)
}
for _, k := range []string{"lans[0].address", "lans[0].vlan", "lans[1].name"} {
if fe[k] == "" {
t.Errorf("no error for %s: %v", k, fe)
}
}
}
func TestApplyAddsAndRemovesLANs(t *testing.T) {
s := mustParse(t)
f := s.form()
f.LANs = append(f.LANs, LANForm{Name: "guest", Interface: "eth2", VLAN: "20", Address: "192.168.30.1",
DHCP: true, PoolStart: "192.168.30.100", PoolEnd: "192.168.30.199"})
if _, err := s.apply(f, "/pw"); err != nil {
t.Fatal(err)
}
if got := s.form().LANs; len(got) != 2 || got[0].Name != "guest" || got[0].VLAN != "20" {
t.Fatalf("after adding: %+v", got)
}
f = s.form()
f.LANs[0].Remove = true
if _, err := s.apply(f, "/pw"); err != nil {
t.Fatal(err)
}
if got := s.form().LANs; len(got) != 1 || got[0].Name != "main" {
t.Fatalf("after removing: %+v", got)
}
f = s.form()
f.LANs[0].Remove = true
if _, err := s.apply(f, "/pw"); err == nil {
t.Fatal("removing the last LAN was accepted")
}
}