Files
nmon/internal/api/helpers_test.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

181 lines
4.9 KiB
Go

package api_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/v10networks/nmon/internal/store"
"github.com/v10networks/nmon/internal/ulid"
)
// device stores a network device and returns it with its generated slug.
func (e *env) device(t *testing.T, name, site, role string, mut ...func(*store.Device)) *store.Device {
t.Helper()
d := &store.Device{Name: name, Kind: store.KindNetwork, Site: site, Role: role, Enabled: true, Profile: "generic", MgmtIP: "192.0.2.1"}
for _, m := range mut {
m(d)
}
if err := e.st.CreateDevice(context.Background(), d); err != nil {
t.Fatal(err)
}
return d
}
// iface stores one interface of d and returns it.
func (e *env) iface(t *testing.T, d *store.Device, idx int64, name, alias string, mut ...func(*store.Interface)) store.Interface {
t.Helper()
i := store.Interface{
ID: ulid.New(e.clk.Now()), DeviceID: d.ID, IfIndex: idx, IfName: name, IfDescr: name, IfAlias: alias,
IfSpeed: 1_000_000_000, IfHighSpeed: 1000, AdminStatus: 1, OperStatus: 1, HasHCCounters: true,
FirstSeen: e.clk.Now(), LastDiscovered: e.clk.Now(),
}
for _, m := range mut {
m(&i)
}
if err := e.st.UpsertInterfaces(context.Background(), d.ID, []store.Interface{i}); err != nil {
t.Fatal(err)
}
return i
}
// getJSON GETs path and decodes a 200 body into v.
func (e *env) getJSON(t *testing.T, path string, v any) *httptest.ResponseRecorder {
t.Helper()
rec := e.do("GET", path, "")
if rec.Code != 200 {
t.Fatalf("GET %s: %d %s", path, rec.Code, rec.Body)
}
if err := json.Unmarshal(rec.Body.Bytes(), v); err != nil {
t.Fatalf("GET %s: %v: %s", path, err, rec.Body)
}
return rec
}
// vector builds an instant-query response from rows of (labels, value).
type vrow struct {
labels map[string]string
v float64
}
func vector(rows ...vrow) []byte {
var b bytes.Buffer
b.WriteString(`{"status":"success","data":{"resultType":"vector","result":[`)
for i, r := range rows {
if i > 0 {
b.WriteByte(',')
}
lb, _ := json.Marshal(r.labels)
fmt.Fprintf(&b, `{"metric":%s,"value":[1,"%v"]}`, lb, r.v)
}
b.WriteString(`]}}`)
return b.Bytes()
}
// matrix builds a range-query response with one series per entry of rows:
// labels and a map of unix second to value.
type mrow struct {
labels map[string]string
vals map[int64]float64
}
func matrix(rows ...mrow) []byte {
var b bytes.Buffer
b.WriteString(`{"status":"success","data":{"resultType":"matrix","result":[`)
for i, r := range rows {
if i > 0 {
b.WriteByte(',')
}
lb, _ := json.Marshal(r.labels)
fmt.Fprintf(&b, `{"metric":%s,"values":[`, lb)
first := true
for _, ts := range sortedKeys(r.vals) {
if !first {
b.WriteByte(',')
}
first = false
fmt.Fprintf(&b, `[%d,"%v"]`, ts, r.vals[ts])
}
b.WriteString(`]}`)
}
b.WriteString(`]}}`)
return b.Bytes()
}
func sortedKeys(m map[int64]float64) []int64 {
out := make([]int64, 0, len(m))
for k := range m {
out = append(out, k)
}
for i := 1; i < len(out); i++ {
for j := i; j > 0 && out[j] < out[j-1]; j-- {
out[j], out[j-1] = out[j-1], out[j]
}
}
return out
}
// emptyMatrix is a range response with no series.
var emptyMatrix = []byte(`{"status":"success","data":{"resultType":"matrix","result":[]}}`)
// walkJSON calls fn for every key/value of every object in the JSON document.
func walkJSON(t *testing.T, body []byte, fn func(path, key string, val any)) {
t.Helper()
var doc any
if err := json.Unmarshal(body, &doc); err != nil {
t.Fatalf("not JSON: %v: %s", err, body)
}
var rec func(path string, v any)
rec = func(path string, v any) {
switch x := v.(type) {
case map[string]any:
for k, c := range x {
fn(path, k, c)
rec(path+"."+k, c)
}
case []any:
for i, c := range x {
rec(fmt.Sprintf("%s[%d]", path, i), c)
}
}
}
rec("$", doc)
}
// assertNoLeaks fails when body has a numeric id, a credential field or a secret.
func assertNoLeaks(t *testing.T, what string, body []byte, secrets ...string) {
t.Helper()
walkJSON(t, body, func(path, key string, val any) {
lower := strings.ToLower(key)
if _, isNum := val.(float64); isNum && (lower == "id" || strings.HasSuffix(lower, "_id") || lower == "device_id") {
t.Errorf("%s: numeric id at %s.%s = %v", what, path, key, val)
}
for _, bad := range []string{"community", "password", "pass", "hash", "secret", "token", "credential"} {
if strings.Contains(lower, bad) {
t.Errorf("%s: credential-like key %s.%s", what, path, key)
}
}
})
for _, s := range secrets {
if bytes.Contains(body, []byte(s)) {
t.Errorf("%s: response contains secret %q", what, s)
}
}
}
var t0 = time.Date(2026, 10, 22, 12, 0, 0, 0, time.UTC)
// doFrom is do with a chosen remote address, which the admission limits key on.
func (e *env) doFrom(method, path, remote string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, nil)
req.RemoteAddr = remote
rec := httptest.NewRecorder()
e.h.ServeHTTP(rec, req)
return rec
}