Add DHCP reservations to the web UI, split out a fast webui test
The DHCP page edits the reservations per LAN (checked like the module's assertions) and offers active clients for reservation. The web UI checks that need no installed system moved from apply.nix into webui.nix (one node, under a minute); apply.nix shares one apply for the pool change and a reservation. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReservationsRoundTrip(t *testing.T) {
|
||||
s := mustParse(t)
|
||||
rows := s.reservations()
|
||||
if len(rows) != 1 || rows[0] != (Reservation{LAN: "main", Name: "nas", MAC: "aa:bb:cc:dd:ee:ff", Address: "192.168.10.5"}) {
|
||||
t.Fatalf("reservations = %+v", rows)
|
||||
}
|
||||
rows = append(rows, Reservation{LAN: "main", Name: "printer", MAC: "AA:BB:CC:00:11:22", Address: "192.168.10.6"},
|
||||
Reservation{LAN: "main"}) // the empty add row
|
||||
if err := s.applyReservations(rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := str(s, "router", "lans", "main", "dhcp", "reservations", "printer", "mac"); got != "aa:bb:cc:00:11:22" {
|
||||
t.Errorf("mac = %q (should be lower case)", got)
|
||||
}
|
||||
// What else the LAN has survives.
|
||||
out, _ := s.marshal()
|
||||
for _, want := range []string{`"fd00::1/64"`, `"pool"`, `"wireguard"`} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Errorf("%s lost:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
rows[0].Remove = true
|
||||
rows[1].Remove = true
|
||||
if err := s.applyReservations(rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := s.reservations(); len(got) != 0 {
|
||||
t.Errorf("not removed: %+v", got)
|
||||
}
|
||||
if !strings.Contains(string(mustMarshal(t, s)), `"pool"`) {
|
||||
t.Error("the pool went with the reservations")
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(t *testing.T, s Settings) []byte {
|
||||
t.Helper()
|
||||
out, err := s.marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestReservationsAreChecked(t *testing.T) {
|
||||
good := Reservation{LAN: "main", Name: "tv", MAC: "aa:bb:cc:dd:ee:01", Address: "192.168.10.7"}
|
||||
for name, mod := range map[string]func(*Reservation){
|
||||
"name": func(r *Reservation) { r.Name = "tv room" },
|
||||
"mac": func(r *Reservation) { r.MAC = "aa:bb:cc" },
|
||||
"address": func(r *Reservation) { r.Address = "192.168.11.7" },
|
||||
"router": func(r *Reservation) { r.Address = "192.168.10.1" },
|
||||
"lan": func(r *Reservation) { r.LAN = "nope" },
|
||||
"dupmac": func(r *Reservation) { r.MAC = "AA:BB:CC:DD:EE:FF" },
|
||||
"dupaddr": func(r *Reservation) { r.Address = "192.168.10.5" },
|
||||
"dupname": func(r *Reservation) { r.Name = "NAS" },
|
||||
} {
|
||||
s := mustParse(t)
|
||||
r := good
|
||||
mod(&r)
|
||||
rows := append(s.reservations(), r)
|
||||
if err := s.applyReservations(rows); err == nil {
|
||||
t.Errorf("%s: accepted %+v", name, r)
|
||||
}
|
||||
}
|
||||
s := mustParse(t)
|
||||
if err := s.applyReservations(append(s.reservations(), good)); err != nil {
|
||||
t.Errorf("a good reservation was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPPage(t *testing.T) {
|
||||
a, sys := newTestApp(t)
|
||||
h := a.routes()
|
||||
rec := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil)
|
||||
cookie := rec.Result().Cookies()[0]
|
||||
|
||||
body := do(h, "GET", "/dhcp?lan=main&mac=aa:bb:cc:00:00:09&address=192.168.10.9&name=cam", nil, cookie).Body.String()
|
||||
for _, want := range []string{`value="nas"`, `value="aa:bb:cc:00:00:09"`, `value="cam"`} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("page lacks %s:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
token := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1]
|
||||
|
||||
// A lease of an unreserved client is offered for reservation.
|
||||
os.WriteFile(filepath.Join(sys.StateDir, "leases"), []byte(
|
||||
"address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id\n"+
|
||||
"192.168.10.120,aa:bb:cc:00:00:20,,3600,4102444800,1,0,0,phone,0,,0\n"), 0o600)
|
||||
if body := do(h, "GET", "/dhcp", nil, cookie).Body.String(); !strings.Contains(body, "aa:bb:cc:00:00:20") {
|
||||
t.Errorf("the active client is not listed:\n%s", body)
|
||||
}
|
||||
|
||||
form := url.Values{"csrf": {token}, "rows": {"2"},
|
||||
"res.0.lan": {"main"}, "res.0.name": {"nas"}, "res.0.mac": {"aa:bb:cc:dd:ee:ff"}, "res.0.address": {"192.168.10.5"},
|
||||
"res.1.lan": {"main"}, "res.1.name": {"cam"}, "res.1.mac": {"aa:bb:cc:00:00:09"}, "res.1.address": {"192.168.10.300"}}
|
||||
if rec := do(h, "POST", "/dhcp", form, cookie); rec.Code != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("bad address: %d", rec.Code)
|
||||
}
|
||||
form.Set("res.1.address", "192.168.10.9")
|
||||
if rec := do(h, "POST", "/dhcp", form, cookie); rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("save: %d\n%s", rec.Code, rec.Body)
|
||||
}
|
||||
s, _ := loadSettings(sys.settingsPath())
|
||||
if str(s, "router", "lans", "main", "dhcp", "reservations", "cam", "address") != "192.168.10.9" {
|
||||
t.Fatalf("not saved: %v", s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user