package cli import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/notify" "gitea.mynodes.xyz/ilker/llmgw/internal/settings" ) func TestAWebhooksFormatIsWhatItsAddressTakes(t *testing.T) { for in, want := range map[string]string{ "https://hooks.slack.com/services/T1/B2/abc": "slack", "https://chat.example.com/hooks/xyz": "slack", // Mattermost, Rocket.Chat "https://discord.com/api/webhooks/1/abc/slack": "slack", "https://alerts.example.com/llmgw": "json", "json:https://hooks.slack.com/services/T1/B2/abc": "json", "slack:https://alerts.example.com/llmgw": "slack", " https://events.pagerduty.com/integration/x/enqueue ": "json", } { w, err := parseWebhook(in) if err != nil || w.Format != want || strings.HasPrefix(w.URL, "slack:") || strings.HasPrefix(w.URL, "json:") { t.Errorf("%q: %+v %v, want %s", in, w, err, want) } } for _, bad := range []string{"hooks.slack.com/x", "ftp://example.com/x", "slack:", "https://"} { if _, err := parseWebhook(bad); err == nil { t.Errorf("%q was taken", bad) } } } func TestWebhooksAreAddedToTheSettingsBesideTheOnesThere(t *testing.T) { doc, err := settings.FromBytes("settings", []byte("router:\n retries: 2\nnotify:\n webhooks:\n"+ " - url: https://alerts.example.com/a\n events: [node_lost]\n secret_file: /etc/llmgw/secrets/hook\n")) if err != nil { t.Fatal(err) } hooks, _ := parseWebhooks([]string{"https://alerts.example.com/a, https://hooks.slack.com/services/T/B/c"}) changed, err := withWebhooks(doc, hooks) if err != nil || !changed { t.Fatalf("changed %v: %v", changed, err) } b, _ := doc.Bytes() got := string(b) for _, want := range []string{"retries: 2", "url: https://alerts.example.com/a", "events: [node_lost]", "secret_file: /etc/llmgw/secrets/hook", "url: https://hooks.slack.com/services/T/B/c", "format: slack"} { if !strings.Contains(got, want) { t.Errorf("%q not in:\n%s", want, got) } } if strings.Count(got, "alerts.example.com/a") != 1 { t.Errorf("a webhook already there was added again:\n%s", got) } if changed, _ := withWebhooks(doc, hooks); changed { t.Error("adding the same webhooks again changed the settings") } } func TestSetupPutsItsWebhooksInTheSettingsAndTestsThemOnceInForce(t *testing.T) { var mu sync.Mutex body, tests := "router:\n retries: 2\n", 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() defer mu.Unlock() w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/v1/settings": _ = json.NewEncoder(w).Encode(adminapi.SettingsView{Version: 1, Body: body}) case r.Method == http.MethodPut && r.URL.Path == "/api/v1/settings": var req adminapi.SettingsRequest _ = json.NewDecoder(r.Body).Decode(&req) body = req.Body _ = json.NewEncoder(w).Encode(adminapi.SettingsView{Version: 2, Body: body}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/notify/test": tests++ // The first answer comes from the control plane before it // started again on the change: it knows no webhooks. res := []notify.Result{} if tests > 1 { res = []notify.Result{{URL: notify.Redact("https://hooks.slack.com/services/T/B/c")}, {URL: notify.Redact("https://ops.example.com/in"), Err: "502 Bad Gateway"}} } _ = json.NewEncoder(w).Encode(res) default: http.NotFound(w, r) } })) defer srv.Close() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "server.yaml"), []byte("admin_listen: "+strings.TrimPrefix(srv.URL, "http://")+"\n"), 0o600); err != nil { t.Fatal(err) } var out bytes.Buffer in := &serverInit{etcDir: dir, out: &out, webhooks: []string{"https://hooks.slack.com/services/T/B/c", "https://ops.example.com/in"}} err := in.ensureWebhooks(t.Context()) if err == nil || !strings.Contains(err.Error(), "502") || !strings.Contains(err.Error(), "ctl notify test") { t.Errorf("a webhook that did not answer: %v", err) } mu.Lock() defer mu.Unlock() if !strings.Contains(body, "format: slack") || !strings.Contains(body, "url: https://ops.example.com/in") || !strings.Contains(body, "retries: 2") || tests < 2 { t.Errorf("settings %q after %d tests", body, tests) } if strings.Contains(out.String(), "/services/T/B/c") { t.Errorf("a webhook's secret path was printed:\n%s", out.String()) } if !strings.Contains(out.String(), "reached https://hooks.slack.com/…") { t.Errorf("the one that answered is not said:\n%s", out.String()) } }