Files
nmon/internal/api/setup_test.go
Vibe Coder f31fb3712a
Some checks failed
ci / lint (push) Successful in 4m19s
ci / openapi (push) Successful in 29s
ci / postgres (push) Successful in 4m55s
ci / test (push) Failing after 19m48s
Add settings forms, first-install wizard, webhook presets, help and poll tools
Make nmon easier to install and run without editing YAML by hand:

- Settings: point-and-click forms over config.yaml (GET/PUT
  /api/v1/settings/form); the YAML editor moves to /settings/yaml.
- First install: with no sign-in configured nmon starts in install
  mode, serves only the setup wizard (sign-in, basics, devices,
  alerting) and closes it for good once sign-in is saved. A
  self-signed certificate is generated when no TLS source is given.
  poller.interval becomes a config setting.
- Reverse proxies: opt-in --http-listen plain-HTTP listener beside
  HTTPS; sign-in stays required.
- Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets,
  template preview and test send.
- Help: the operations, coverage and profile docs served at /help.
- Polling: per-device "Poll now", a dry-run test poll returning an
  SNMP trace, and time-limited in-memory SNMP debug recording, in the
  UI and as `nmon device poll`.
- Ansible: deploys into install mode when no sign-in is set, and no
  longer overwrites the config.yaml the wizard wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
2026-10-25 06:31:12 +00:00

96 lines
3.6 KiB
Go

package api_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/v10networks/nmon/internal/api"
"github.com/v10networks/nmon/internal/auth"
)
type fakeSetup struct {
install bool
done bool
}
func (f *fakeSetup) InstallMode() bool { return f.install }
func (f *fakeSetup) SetupCompleted(context.Context) (bool, error) { return f.done, nil }
func (f *fakeSetup) SignIn(context.Context, string, api.SetupSignInBody) (api.SetupSignIn, error) {
return api.SetupSignIn{}, api.ErrSetupClosed
}
func (f *fakeSetup) Complete(context.Context, string) (api.SetupComplete, error) {
return api.SetupComplete{}, api.ErrSetupNoSignIn
}
func TestSetupStateIsPublicAndCarriesNoSecrets(t *testing.T) {
fs := &fakeSetup{install: true}
e := newAuthedEnv(t, func(d *api.Deps) { d.Setup = fs })
rec := httptest.NewRecorder()
e.top.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/setup", nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET /setup without credentials = %d %s", rec.Code, rec.Body)
}
var s api.SetupState
if err := json.Unmarshal(rec.Body.Bytes(), &s); err != nil || !s.InstallMode || s.Defaults.BreakGlassUser != "admin" ||
s.Defaults.MinSignInLength != auth.MinBreakGlassPassword || s.Defaults.PollInterval != "5m" {
t.Fatalf("state = %s (%v)", rec.Body, err)
}
if strings.Contains(strings.ToLower(rec.Body.String()), "password\":") {
t.Fatalf("state leaks a password field: %s", rec.Body)
}
}
func TestSetupPostsAnswer409WhenSetupIsClosed(t *testing.T) {
e := newEnvDeps(t, 0, func(d *api.Deps) { d.Setup = &fakeSetup{done: true}; d.AlertPolicy = &fakePolicyHost{} })
isProblem(t, e.do("POST", "/api/v1/setup/signin", `{"username":"admin","password":"a long enough one"}`), http.StatusConflict)
isProblem(t, e.do("POST", "/api/v1/setup/recommended-policies", `{}`), http.StatusConflict)
isProblem(t, e.do("POST", "/api/v1/setup/complete", ``), http.StatusConflict)
}
// allRulesHost knows every rule the recommended set binds.
type allRulesHost struct{ fakePolicyHost }
func (allRulesHost) Rules() []api.AlertRuleInfo {
var out []api.AlertRuleInfo
for name, level := range map[string]string{"device_down": "device", "device_warning": "device", "interface_errors": "interface",
"cpu_high": "device", "memory_high": "device", "disk_full": "component"} {
out = append(out, api.AlertRuleInfo{Name: name, Level: level, Severity: "warning"})
}
return out
}
func TestRecommendedPoliciesAreCreatedOnceAndIdempotently(t *testing.T) {
host := &allRulesHost{}
e := newEnvDeps(t, 0, func(d *api.Deps) { d.Setup = &fakeSetup{install: true}; d.AlertPolicy = host })
var first, second api.SetupPolicies
rec := e.do("POST", "/api/v1/setup/recommended-policies", `{}`)
if rec.Code != http.StatusOK || json.Unmarshal(rec.Body.Bytes(), &first) != nil || len(first.Created) != 6 {
t.Fatalf("first run: %d %s", rec.Code, rec.Body)
}
rec = e.do("POST", "/api/v1/setup/recommended-policies", `{}`)
if rec.Code != http.StatusOK || json.Unmarshal(rec.Body.Bytes(), &second) != nil || len(second.Created) != 0 || len(second.Existing) != 6 {
t.Fatalf("second run must create nothing: %d %s", rec.Code, rec.Body)
}
var list api.AlertPolicyList
e.getJSON(t, "/api/v1/alert-policies", &list)
if len(list.Items) != 6 || !list.Items[0].Scope.All || !list.Items[0].Enabled {
t.Fatalf("policies = %+v", list)
}
}
func TestInstallRoutesAreRealRoutes(t *testing.T) {
have := map[string]bool{}
for _, r := range api.Routes() {
have[r.Pattern] = true
}
for _, pat := range api.InstallRoutes() {
if !have[pat] {
t.Errorf("install mode allows %q, which is not an API route", pat)
}
}
}