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
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// rest sends a request to the REST API with a token.
|
||||
@@ -99,15 +100,19 @@ func TestLogs(t *testing.T) {
|
||||
want []string
|
||||
bad bool
|
||||
}{
|
||||
{logQuery{}, []string{"--lines=100"}, false},
|
||||
{logQuery{Unit: "router-webui"}, []string{"--lines=100"}, false},
|
||||
{logQuery{}, nil, true},
|
||||
{logQuery{Unit: "sshd.service"}, []string{"--unit=sshd.service"}, false},
|
||||
{logQuery{Unit: "systemd-journald"}, nil, true},
|
||||
{logQuery{Unit: "getty@tty1"}, nil, true},
|
||||
{logQuery{Unit: "router-pppoe", Lines: 5, Since: "1 hour ago", Priority: "err", Grep: "LCP", Boot: &one},
|
||||
[]string{"--lines=5", "--unit=router-pppoe", "--since=1 hour ago", "--priority=err", "--grep=LCP", "--boot=0"}, false},
|
||||
{logQuery{Unit: "kernel"}, []string{"-k"}, false},
|
||||
{logQuery{Unit: "--vacuum-time=1s"}, nil, true},
|
||||
{logQuery{Unit: "a b"}, nil, true},
|
||||
{logQuery{Lines: 5000}, nil, true},
|
||||
{logQuery{Priority: "loud"}, nil, true},
|
||||
{logQuery{Since: "$(reboot)"}, nil, true},
|
||||
{logQuery{Unit: "router-webui", Lines: 5000}, nil, true},
|
||||
{logQuery{Unit: "router-webui", Priority: "loud"}, nil, true},
|
||||
{logQuery{Unit: "router-webui", Since: "$(reboot)"}, nil, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
args, err := journalArgs(c.q)
|
||||
@@ -178,3 +183,54 @@ func TestOpenAPI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenExpiry(t *testing.T) {
|
||||
a, _ := newTestApp(t)
|
||||
h := a.routes()
|
||||
tok, secret, err := a.tokens.CreateFor("short", "read", time.Hour)
|
||||
if err != nil || tok.Expires.IsZero() {
|
||||
t.Fatalf("create: %v %+v", err, tok)
|
||||
}
|
||||
if rec := rest(h, secret, "GET", "/api/v1/status", "", nil); rec.Code != 200 {
|
||||
t.Fatalf("valid token: %d", rec.Code)
|
||||
}
|
||||
// Move its end into the past.
|
||||
toks := a.tokens.load()
|
||||
toks[0].Expires = time.Now().Add(-time.Minute)
|
||||
if err := a.tokens.save(toks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec := rest(h, secret, "GET", "/api/v1/status", "", nil); rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expired token: %d", rec.Code)
|
||||
}
|
||||
if rec, _ := rpc(h, secret, "ping", nil); rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expired token on MCP: %d", rec.Code)
|
||||
}
|
||||
_, forever, _ := a.tokens.Create("long", "read")
|
||||
if rec := rest(h, forever, "GET", "/api/v1/status", "", nil); rec.Code != 200 {
|
||||
t.Fatalf("token without expiry: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenEditNote(t *testing.T) {
|
||||
a, sys := newTestApp(t)
|
||||
h := a.routes()
|
||||
data, _ := os.ReadFile(sys.settingsPath())
|
||||
os.WriteFile(sys.confirmedPath(), data, 0o600)
|
||||
_, tok, _ := a.tokens.Create("assistant", "change")
|
||||
rec := rest(h, tok, "GET", "/api/v1/settings", "", nil)
|
||||
e := rec.Header().Get("ETag")
|
||||
if rec := rest(h, tok, "PATCH", "/api/v1/settings", `{"system":{"hostName":"gw"}}`, map[string]string{"If-Match": e}); rec.Code != 200 {
|
||||
t.Fatalf("patch: %d %s", rec.Code, rec.Body)
|
||||
}
|
||||
cur, _ := os.ReadFile(sys.settingsPath())
|
||||
if note := sys.tokenEditNote(cur); !strings.Contains(note, "assistant") {
|
||||
t.Fatalf("no note: %q", note)
|
||||
}
|
||||
// A later edit by a person makes the note stale.
|
||||
other := append([]byte(nil), cur...)
|
||||
other = append(other, '\n', ' ')
|
||||
if note := sys.tokenEditNote(other); note != "" {
|
||||
t.Fatalf("stale note: %q", note)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user