package webhook import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) func samplePayload() Payload { return Payload{ Timestamp: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), ParentDir: "/home/user/projects", Summary: status.Summary{Total: 3, Clean: 1, Attention: 2, Errored: 0}, Changed: []status.RepoInfo{ {Name: "api", Branch: "main", Status: status.StatusBehind, BehindBy: 5}, {Name: "web", Branch: "feat/login", Status: status.StatusModified, ModifiedFiles: []string{"a.go"}, AheadBy: 3}, }, } } func TestStatusSymbol(t *testing.T) { cases := []struct { s status.RepoStatus want string }{ {status.StatusClean, "✅"}, {status.StatusModified, "✏️"}, {status.StatusAhead, "⬆️"}, {status.StatusBehind, "⬇️"}, {status.StatusDiverged, "🔀"}, {status.StatusDetached, "📌"}, {status.StatusBare, "📦"}, {status.StatusError, "❌"}, } for _, tc := range cases { if got := statusSymbol(tc.s); got != tc.want { t.Errorf("statusSymbol(%v) = %q, want %q", tc.s, got, tc.want) } } } func TestNewSender(t *testing.T) { for _, typ := range []string{"slack", "discord", "generic"} { s, err := NewSender(Config{Name: "test", Type: typ, URL: "http://localhost"}) if err != nil { t.Errorf("NewSender(%q): %v", typ, err) } if s.Name() != "test" { t.Errorf("NewSender(%q).Name() = %q, want test", typ, s.Name()) } } if _, err := NewSender(Config{Name: "bad", Type: "nope"}); err == nil { t.Error("NewSender(nope) succeeded, want error") } } func TestSlackSender(t *testing.T) { var got slackMessage srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Fatal(err) } _, _ = w.Write([]byte("{}")) })) defer srv.Close() s := newSlack(Config{Name: "test", URL: srv.URL}) if err := s.Send(t.Context(), samplePayload()); err != nil { t.Fatalf("Send: %v", err) } if len(got.Blocks) < 3 { t.Fatalf("got %d blocks, want >= 3", len(got.Blocks)) } body := got.Blocks[1].Text.Text if !strings.Contains(body, "3 repos") || !strings.Contains(body, "1 clean") { t.Errorf("summary = %q", body) } } func TestDiscordSender(t *testing.T) { var got discordMessage srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Fatal(err) } _, _ = w.Write([]byte("{}")) })) defer srv.Close() s := newDiscord(Config{Name: "test", URL: srv.URL}) if err := s.Send(t.Context(), samplePayload()); err != nil { t.Fatalf("Send: %v", err) } if len(got.Embeds) != 1 { t.Fatalf("got %d embeds, want 1", len(got.Embeds)) } emb := got.Embeds[0] if emb.Title != "gitflow scan" { t.Errorf("title = %q", emb.Title) } if !strings.Contains(emb.Description, "3 repos") { t.Errorf("description = %q", emb.Description) } if emb.Color != 16776960 { t.Errorf("color = %d, want 16776960 (yellow/attention)", emb.Color) } } func TestDiscordErrorColor(t *testing.T) { p := samplePayload() p.Summary.Errored = 1 var got discordMessage srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) _, _ = w.Write([]byte("{}")) })) defer srv.Close() s := newDiscord(Config{Name: "test", URL: srv.URL}) _ = s.Send(t.Context(), p) if got.Embeds[0].Color != 15158332 { t.Errorf("color = %d, want 15158332 (red/errors)", got.Embeds[0].Color) } } func TestGenericSender(t *testing.T) { var got Payload srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{}`)) })) defer srv.Close() s := newGeneric(Config{Name: "test", URL: srv.URL}) if err := s.Send(t.Context(), samplePayload()); err != nil { t.Fatalf("Send: %v", err) } if got.Summary.Total != 3 { t.Errorf("payload.Summary.Total = %d, want 3", got.Summary.Total) } } func TestDispatcherOnChangeOnly(t *testing.T) { var mu sync.Mutex var paths []string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var p Payload json.NewDecoder(r.Body).Decode(&p) mu.Lock() for _, r := range p.Changed { paths = append(paths, r.Name) } mu.Unlock() _, _ = w.Write([]byte("{}")) })) defer srv.Close() d := NewDispatcher([]Config{{ Name: "test", Type: "generic", URL: srv.URL, OnChangeOnly: true, }}) payload := samplePayload() // No changes → skipped. errs := d.Dispatch(context.Background(), payload, 0) if len(errs) > 0 { t.Fatalf("Dispatch: %v", errs) } mu.Lock() noChangeCalls := len(paths) mu.Unlock() if noChangeCalls != 0 { t.Errorf("dispatch with changedCount=0 sent %d requests, want 0", noChangeCalls) } } func TestHTTPErrorSurfaced(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) })) defer srv.Close() s := newGeneric(Config{Name: "test", URL: srv.URL, Retry: RetryConfig{MaxAttempts: 1}}) if err := s.Send(t.Context(), samplePayload()); err == nil { t.Error("Send(500) succeeded, want error") } } func TestRetryOnTransientErrors(t *testing.T) { var mu sync.Mutex var attempts int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() attempts++ n := attempts mu.Unlock() if n < 3 { http.Error(w, "gateway timeout", http.StatusGatewayTimeout) return } _, _ = w.Write([]byte("{}")) })) defer srv.Close() d := NewDispatcher([]Config{{ Name: "test", Type: "generic", URL: srv.URL, Retry: RetryConfig{MaxAttempts: 3, Backoff: 1 * time.Millisecond}, }}) errs := d.Dispatch(context.Background(), samplePayload(), 1) if len(errs) > 0 { t.Fatalf("Dispatch: %v", errs) } mu.Lock() if attempts != 3 { t.Errorf("attempts = %d, want 3", attempts) } mu.Unlock() } func TestDispatcherMultipleSenders(t *testing.T) { var mu sync.Mutex var s1, s2 bool srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() s1 = true mu.Unlock() _, _ = w.Write([]byte("{}")) })) defer srv1.Close() srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() s2 = true mu.Unlock() _, _ = w.Write([]byte("{}")) })) defer srv2.Close() d := NewDispatcher([]Config{ {Name: "w1", Type: "generic", URL: srv1.URL}, {Name: "w2", Type: "generic", URL: srv2.URL}, }) errs := d.Dispatch(context.Background(), samplePayload(), 1) if len(errs) > 0 { t.Fatalf("Dispatch: %v", errs) } mu.Lock() if !s1 || !s2 { t.Errorf("s1=%v s2=%v, want both true", s1, s2) } mu.Unlock() }