Skip to content
quarry Artifact [838ce40dfa]
Login

Artifact [838ce40dfa]

Artifact 838ce40dfad84c4cccf40a1cef6a6a6edefeb33e4c9aee2840831f27b6073438:


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