Files
nmon/internal/api/notify_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

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)
}
}