Files
nixos-router/pkgs/router-webui/interfaces_test.go
T
nilsandClaude Sonnet 5.5 b1fa7b1396 Add token expiry, edit notice, log allowlist, local-zone test; merge PLAN.md into SPECS.md
get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd,
nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30
(default) or 90 days, or without limit. The overview and the Changes page
say which token made the unapplied changes.

options.json (router://options) no longer holds the options' source paths:
they differ between builds of a release, so a router rebuilding itself
offline had to build the file and failed (found by the update test).

Web UI: panels flow into columns on wide screens and the width is no longer
capped; on phones the navigation is a menu button; the Advanced page uses
panels; VLAN dropdowns show their chosen names; the duplicate WireGuard
heading is gone.

basic: a subtest for wan -> local rules.

SPECS.md is now the only document (decisions, how each part works, and what
is not done yet); PLAN.md is gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-11-01 19:06:27 +01:00

141 lines
4.6 KiB
Go

package main
import (
"strings"
"testing"
)
func TestChosenName(t *testing.T) {
for _, ok := range []string{"wan", "lan", "dmz", "guest-wifi", "isp1", "a"} {
if why := chosenName(ok); why != "" {
t.Errorf("%q refused: %s", ok, why)
}
}
for _, bad := range []string{"", "Wan", "1wan", "wan_1", "eth0", "eth", "wlan0", "enp2s0", "eno1", "ens3", "wlp1s0", "ppp0", "wg0", "veth12", "br0", "lo", "this-name-is-too-long"} {
if chosenName(bad) == "" {
t.Errorf("%q accepted", bad)
}
}
}
func renameSettings(t *testing.T) Settings {
t.Helper()
s, err := parseSettings([]byte(`{"router": {
"wan": {"interface": "enp1s0", "ipv4": {"type": "pppoe", "pppoe": {"vlan": 7}}},
"lans": {"lan": {"interface": "enp2s0"}, "dmz": {"interface": "enp2s0", "vlan": 1002}, "iot": {"interface": "enp3s0", "vlan": 5}}}}`))
if err != nil {
t.Fatal(err)
}
return s
}
func kernel(mac string) string { return map[string]string{"aa:aa": "enp1s0", "bb:bb": "enp2s0"}[mac] }
func TestRenameCard(t *testing.T) {
s := renameSettings(t)
moved, err := s.rename([]Rename{{Dev: "enp2s0", MAC: "bb:bb", From: "enp2s0", To: "lan"}}, kernel)
if err != nil || moved["enp2s0"] != "lan" {
t.Fatalf("moved %v, err %v", moved, err)
}
if s.cardNames()["lan"] != "bb:bb" {
t.Errorf("cards = %v", s.cardNames())
}
f := s.form()
for _, l := range f.LANs {
if (l.Name == "lan" || l.Name == "dmz") && !strings.HasPrefix(l.Interface, "lan") {
t.Errorf("LAN %s still on %s", l.Name, l.Interface)
}
if l.Name == "iot" && l.Interface != "enp3s0.5" {
t.Errorf("an unrelated LAN moved: %s", l.Interface)
}
}
// The card is now called lan; calling it wan renames the entry, not adds one.
if _, err := s.rename([]Rename{{Dev: "lan", MAC: "bb:bb", From: "lan", To: "uplink2"}}, kernel); err != nil {
t.Fatal(err)
}
if got := s.cardNames(); len(got) != 1 || got["uplink2"] != "bb:bb" {
t.Errorf("cards = %v", got)
}
// Clearing the name goes back to the kernel's.
if _, err := s.rename([]Rename{{Dev: "uplink2", MAC: "bb:bb", From: "uplink2", To: ""}}, kernel); err != nil {
t.Fatal(err)
}
if _, ok := s["router"].(map[string]any)["interfaces"]; ok {
t.Error("an empty interfaces table is kept")
}
if got := s.form().LANs[0].Interface; got != "enp2s0" && got != "enp2s0.1002" {
t.Errorf("LAN back on %s", got)
}
}
func TestRenameRefused(t *testing.T) {
for name, row := range map[string]Rename{
"reserved": {Dev: "enp2s0", MAC: "bb:bb", From: "enp2s0", To: "eth5"},
"not there": {Dev: "enp9s0", From: "enp9s0", To: "lan"},
"unused": {Dev: "enp9s0.4", From: "", To: "x"},
} {
s := renameSettings(t)
if _, err := s.rename([]Rename{row}, kernel); err == nil {
t.Errorf("%s accepted", name)
}
}
s := renameSettings(t)
_, err := s.rename([]Rename{
{Dev: "enp1s0", MAC: "aa:aa", From: "enp1s0", To: "wan"},
{Dev: "enp2s0", MAC: "bb:bb", From: "enp2s0", To: "wan"},
}, kernel)
if err == nil {
t.Error("the same name for two cards")
}
}
func TestRenameVLAN(t *testing.T) {
s := renameSettings(t)
if _, err := s.rename([]Rename{
{Dev: "enp2s0.1002", From: "", To: "dmz"},
{Dev: "enp1s0.7", From: "", To: "isp"},
}, kernel); err != nil {
t.Fatal(err)
}
if got := s.vlanNames(); got["enp2s0.1002"] != "dmz" || got["enp1s0.7"] != "isp" || len(got) != 2 {
t.Errorf("vlan names = %v", got)
}
// The identifier in the form is unchanged.
for _, l := range s.form().LANs {
if l.Name == "dmz" && l.Interface != "enp2s0.1002" {
t.Errorf("form identifier = %s", l.Interface)
}
}
// Saving the network form keeps the names; dropping the VLAN drops its name.
f := s.form()
if _, err := s.apply(f, "/pw"); err != nil && !strings.Contains(err.Error(), "lans") {
_ = err
}
if s.vlanNames()["enp2s0.1002"] != "dmz" {
t.Error("the form dropped the VLAN's name")
}
for i, l := range f.LANs {
if l.Name == "dmz" {
f.LANs[i].Interface = "enp2s0"
}
}
_, _ = s.apply(f, "/pw")
if s.vlanNames()["enp2s0.1002"] != "" {
t.Error("an untagged LAN keeps a VLAN device name")
}
// A name already used by a card is refused.
s = renameSettings(t)
if _, err := s.rename([]Rename{{Dev: "enp2s0", MAC: "bb:bb", From: "enp2s0", To: "dmz"}, {Dev: "enp2s0.1002", From: "", To: "dmz"}}, kernel); err == nil {
t.Error("a card and a VLAN with the same name")
}
}
func TestDevLabel(t *testing.T) {
devs := []Device{{Name: "lan.30", Kind: "VLAN", Custom: "cams"}, {Name: "lan.40", Kind: "VLAN"}, {Name: "lan", Kind: "physical"}}
for name, want := range map[string]string{"lan.30": "cams (lan.30)", "lan.40": "lan.40", "lan": "lan", "other": "other"} {
if got := devLabel(devs, name); got != want {
t.Errorf("%s: %q, want %q", name, got, want)
}
}
}