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
141 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|