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
78 lines
3.5 KiB
Go
78 lines
3.5 KiB
Go
package api_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/v10networks/nmon/internal/api"
|
|
"github.com/v10networks/nmon/internal/auth"
|
|
"github.com/v10networks/nmon/internal/auth/authtest"
|
|
"github.com/v10networks/nmon/internal/config"
|
|
"github.com/v10networks/nmon/internal/notify"
|
|
)
|
|
|
|
type fakeNotify struct {
|
|
got notify.Receiver
|
|
state string
|
|
err error
|
|
}
|
|
|
|
func (f *fakeNotify) SendTest(_ context.Context, r notify.Receiver, state string) (notify.TestResult, error) {
|
|
f.got, f.state = r, state
|
|
return notify.TestResult{Payload: "{}", StatusCode: 200, OK: true}, f.err
|
|
}
|
|
|
|
func TestNotifyPresetsPreviewAndTestSend(t *testing.T) {
|
|
fs := &formSettings{cfg: config.Default()}
|
|
fn := &fakeNotify{}
|
|
e := newAuthedEnv(t, func(d *api.Deps) { d.Settings = fs; d.Notify = fn })
|
|
admin, _ := authtest.Mint(t, e.auth, auth.RoleAdmin)
|
|
viewer, _ := authtest.Mint(t, e.auth, auth.RoleViewer)
|
|
|
|
var presets []api.NotifyPreset
|
|
rec := e.call(http.MethodGet, "/api/v1/notify/presets", "", viewer)
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &presets); err != nil || len(presets) != 4 || presets[1].ID != "teams" {
|
|
t.Fatalf("presets: %d %s", rec.Code, rec.Body)
|
|
}
|
|
body := func(tpl string) string {
|
|
b, _ := json.Marshal(map[string]any{"state": "resolved", "receiver": map[string]any{
|
|
"name": "teams", "kind": "webhook", "url": "https://x.example/", "template": tpl, "headers": map[string]string{"Authorization": ""}, "smtp": map[string]any{"clear_password": false, "from": "", "host": "", "password": "", "port": 0,
|
|
"starttls": false, "to": []string{}, "username": ""}}})
|
|
return string(b)
|
|
}
|
|
for _, path := range []string{"/api/v1/notify/preview", "/api/v1/notify/test"} {
|
|
if code := e.call(http.MethodPost, path, body(presets[1].Template), viewer).Code; code != http.StatusForbidden {
|
|
t.Fatalf("viewer POST %s: %d, want 403", path, code)
|
|
}
|
|
}
|
|
var pv api.NotifyPreview
|
|
rec = e.call(http.MethodPost, "/api/v1/notify/preview", body(presets[1].Template), admin)
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &pv); err != nil || pv.Error != "" || !strings.Contains(pv.Output, "AdaptiveCard") {
|
|
t.Fatalf("preview: %d %s", rec.Code, rec.Body)
|
|
}
|
|
rec = e.call(http.MethodPost, "/api/v1/notify/preview", body("{{.Nope}}"), admin)
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &pv); err != nil || rec.Code != http.StatusOK || pv.Error == "" || pv.Output != "" {
|
|
t.Fatalf("broken template: %d %s", rec.Code, rec.Body)
|
|
}
|
|
|
|
// Blank header value resolves to the stored receiver's secret.
|
|
fs.cfg.Alerting.Receivers = nil
|
|
put := `{"receivers":[{"name":"teams","kind":"webhook","url":"https://x.example/","headers":{"Authorization":"Bearer s3cret"},"template":"","smtp":{"clear_password":false,"from":"","host":"","password":"","port":0,"starttls":false,"to":[],"username":""}}]}`
|
|
if rec := e.call(http.MethodPut, "/api/v1/settings/form", put, admin); rec.Code != http.StatusOK {
|
|
t.Fatalf("seed receiver: %d %s", rec.Code, rec.Body)
|
|
}
|
|
rec = e.call(http.MethodPost, "/api/v1/notify/test", body(presets[1].Template), admin)
|
|
var tr api.NotifyTestResult
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &tr); err != nil || !tr.OK || fn.state != "resolved" ||
|
|
fn.got.Headers["Authorization"] != "Bearer s3cret" || strings.Contains(rec.Body.String(), "s3cret") {
|
|
t.Fatalf("test: %d %s got=%+v", rec.Code, rec.Body, fn.got)
|
|
}
|
|
fn.err = notify.ErrTestBusy
|
|
if code := e.call(http.MethodPost, "/api/v1/notify/test", body(""), admin).Code; code != http.StatusTooManyRequests {
|
|
t.Fatalf("busy: %d", code)
|
|
}
|
|
}
|