package webhook
import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"sync"
"testing"
"time"
"craftdesign.group/quarry/internal/events"
)
type settled struct {
ok bool
why string
}
type fakeSettler struct {
mu sync.Mutex
got map[int64][]settled
}
func (f *fakeSettler) Settled(sub int64, ok bool, why string) {
f.mu.Lock()
defer f.mu.Unlock()
if f.got == nil {
f.got = map[int64][]settled{}
}
f.got[sub] = append(f.got[sub], settled{ok, why})
}
func (f *fakeSettler) of(sub int64) []settled {
f.mu.Lock()
defer f.mu.Unlock()
return append([]settled(nil), f.got[sub]...)
}
func fastDispatcher(t *testing.T, subs Subscriptions, tries int) (*Dispatcher, *fakeSettler) {
t.Helper()
ev, err := events.Start(filepath.Join(t.TempDir(), "nats"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
d, err := New(ev.JS(), subs)
if err != nil {
t.Fatal(err)
}
d.maxTries = tries
d.backoff = func(int) time.Duration { return 10 * time.Millisecond }
s := &fakeSettler{}
d.Settle(s)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
if err := d.Start(ctx); err != nil {
t.Fatal(err)
}
return d, s
}
func waitFor(t *testing.T, what string, ok func() bool) {
t.Helper()
deadline := time.Now().Add(20 * time.Second)
for time.Now().Before(deadline) {
if ok() {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("timed out: %s", what)
}
// A delivery that fails every one of its own retries is ONE failure,
// reported once; a delivery that succeeds on a retry is a success.
func TestSettlesOncePerDelivery(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer srv.Close()
subs := &fakeSubs{subs: map[int64]Sub{1: {ID: 1, URL: srv.URL, Secret: "k", Active: true}}}
d, s := fastDispatcher(t, subs, 3)
for _, id := range []string{"e1", "e2"} {
if err := d.Raise(Event{ID: id, Repo: "alpha", Kind: "wiki.save"}); err != nil {
t.Fatal(err)
}
}
waitFor(t, "two settled failures", func() bool { return len(s.of(1)) >= 2 })
time.Sleep(200 * time.Millisecond)
got := s.of(1)
if len(got) != 2 {
t.Fatalf("%d settlements for two deliveries of three tries each: %+v", len(got), got)
}
for _, g := range got {
if g.ok || g.why == "" {
t.Errorf("settlement = %+v", g)
}
}
}
func TestSuccessOnARetrySettlesOK(t *testing.T) {
var mu sync.Mutex
n := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
mu.Lock()
n++
c := n
mu.Unlock()
if c < 2 {
w.WriteHeader(http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
subs := &fakeSubs{subs: map[int64]Sub{1: {ID: 1, URL: srv.URL, Secret: "k", Active: true}}}
d, s := fastDispatcher(t, subs, 3)
if err := d.Raise(Event{ID: "e1", Repo: "alpha", Kind: "wiki.save"}); err != nil {
t.Fatal(err)
}
waitFor(t, "a settlement", func() bool { return len(s.of(1)) >= 1 })
time.Sleep(100 * time.Millisecond)
got := s.of(1)
if len(got) != 1 || !got[0].ok {
t.Fatalf("settlements = %+v", got)
}
}
// A transport error (nothing listening) counts like a non-2xx.
func TestTransportErrorSettlesAsFailure(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
url := srv.URL
srv.Close()
subs := &fakeSubs{subs: map[int64]Sub{1: {ID: 1, URL: url, Secret: "k", Active: true}}}
d, s := fastDispatcher(t, subs, 2)
if err := d.Raise(Event{ID: "e1", Repo: "alpha", Kind: "wiki.save"}); err != nil {
t.Fatal(err)
}
waitFor(t, "a settlement", func() bool { return len(s.of(1)) >= 1 })
if got := s.of(1); got[0].ok || got[0].why == "" {
t.Fatalf("settlements = %+v", got)
}
}