package main import ( "archive/tar" "compress/gzip" "fmt" "io" "math" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "testing" "time" ) func TestTierAveragesAndGaps(t *testing.T) { s := newStore() base := time.Unix(1_800_000_000, 0) // a multiple of every step for i := range 12 { // one minute of 5 s samples s.Add("x", base.Add(time.Duration(i)*5*time.Second), float64(i)) } // A gap of 10 minutes, then one more sample. s.Add("x", base.Add(11*time.Minute), 100) now := base.Add(11 * time.Minute) start, step, vals := s.Read("x", 0, now) if step != 5*time.Second || len(vals) != 720 { t.Fatalf("step %v, %d values", step, len(vals)) } at := func(ts time.Time) float64 { return vals[int(ts.Sub(start)/step)] } if at(base) != 0 || at(base.Add(55*time.Second)) != 11 { t.Fatalf("5 s values: %v %v", at(base), at(base.Add(55*time.Second))) } if !math.IsNaN(at(base.Add(5 * time.Minute))) { t.Fatal("the gap is not empty") } if at(now) != 100 || !math.IsNaN(at(base.Add(-5*time.Second))) { t.Fatal("latest bucket or before the first sample") } // The minute tier averages the first minute. start, step, vals = s.Read("x", 1, now) if v := vals[int(base.Sub(start)/step)]; v != 5.5 { t.Fatalf("minute average %v", v) } // An hour later the 5 s ring has turned over: the old values are gone. later := now.Add(2 * time.Hour) s.Add("x", later, 1) _, _, vals = s.Read("x", 0, later) for i, v := range vals[:len(vals)-1] { if !math.IsNaN(v) { t.Fatalf("stale value %v at %d", v, i) } } } func TestStoreSurvivesSaving(t *testing.T) { path := filepath.Join(t.TempDir(), "m.gob") s := newStore() now := time.Unix(1_800_000_000, 0) s.Add("x", now, 42) s.SetConnected(true, now, "198.51.100.7") if err := s.Save(path); err != nil { t.Fatal(err) } l, err := loadStore(path) if err != nil { t.Fatal(err) } _, _, vals := l.Read("x", 0, now) connected, since, events := l.Uplink() if vals[len(vals)-1] != 42 || !connected || !since.Equal(now) || len(events) != 1 { t.Fatalf("loaded %v %v %v %v", vals[len(vals)-1], connected, since, events) } } const leaseFile = `address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id 192.168.10.101,52:54:00:12:34:56,01:52,3600,%d,1,0,0,laptop,0,,0 192.168.10.100,52:54:00:aa:bb:cc,01:52,3600,%d,1,0,0,old,name,0,,0 192.168.10.100,52:54:00:aa:bb:cc,01:52,3600,%d,1,0,0,phone,0,,0 192.168.10.102,52:54:00:dd:ee:ff,01:52,0,%d,1,0,0,gone,0,,0 192.168.10.103,52:54:00:dd:ee:00,01:52,3600,%d,1,0,0,expired,0,,0 ` func TestReadLeases(t *testing.T) { now := time.Unix(1_800_000_000, 0) future, past := now.Unix()+1800, now.Unix()-10 path := filepath.Join(t.TempDir(), "dhcp4.leases") os.WriteFile(path, []byte(fmt.Sprintf(leaseFile, future, future, future, future, past)), 0o600) lans := []LANForm{{Name: "main", Address: "192.168.10.1"}} got := readLeases(path, lans, now) if len(got) != 2 || got[0].Address != "192.168.10.100" || got[0].Hostname != "phone" || got[1].Hostname != "laptop" || got[1].LAN != "main" { t.Fatalf("leases: %+v", got) } } func csrfFrom(t *testing.T, page string) string { t.Helper() m := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(page) if m == nil { t.Fatalf("no CSRF token in:\n%s", page) } return m[1] } func TestParsePingTargets(t *testing.T) { got, err := parsePingTargets("gateway, 1.1.1.1\n2606:4700:4700::1111 1.1.1.1") if err != nil || strings.Join(got, " ") != "gateway 1.1.1.1 2606:4700:4700::1111" { t.Fatalf("%v %v", got, err) } if _, err := parsePingTargets("example.com"); err == nil { t.Fatal("host names are not accepted") } if _, err := parsePingTargets("1.1.1.1 1.1.1.2 1.1.1.3 1.1.1.4 1.1.1.5 1.1.1.6"); err == nil { t.Fatal("too many targets accepted") } } func login(t *testing.T, h http.Handler) *http.Cookie { t.Helper() rec := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil) if rec.Code != http.StatusSeeOther { t.Fatalf("login: %d", rec.Code) } return rec.Result().Cookies()[0] } func TestDashboardRenders(t *testing.T) { a, _ := newTestApp(t) now := time.Now() for i := range 20 { at := now.Add(time.Duration(i-20) * 5 * time.Second) a.metrics.store.Add("if:eth1:rx", at, 1e6) a.metrics.store.Add("if:eth1:tx", at, 2e5) } h := a.routes() cookie := login(t, h) for _, path := range []string{"/", "/?range=7d", "/dashboard?range=24h", "/advanced"} { rec := do(h, "GET", path, nil, cookie) if rec.Code != http.StatusOK { t.Fatalf("%s: %d\n%s", path, rec.Code, rec.Body) } body := rec.Body.String() if path == "/" && (!strings.Contains(body, "Internet (eth1)") || !strings.Contains(body, "8.0 Mbit/s") || !strings.Contains(body, "/static/dashboard.js")) { t.Fatalf("overview lacks the traffic chart:\n%s", body) } if strings.HasPrefix(path, "/dashboard") && strings.Contains(body, "