mirror of
https://github.com/hanzo-fi/webhooks.git
synced 2026-10-30 20:32:55 +00:00
242 lines
6.7 KiB
Go
242 lines
6.7 KiB
Go
//go:build it
|
|
|
|
package test_suite
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v2/bun/bunconnect"
|
|
"github.com/hanzo-fi/go-libs/v2/logging"
|
|
"github.com/hanzo-fi/go-libs/v2/testing/platform/pgtesting"
|
|
"github.com/hanzo-fi/webhooks/pkg/client/models/components"
|
|
"github.com/hanzo-fi/webhooks/pkg/testserver"
|
|
|
|
webhooks "github.com/hanzo-fi/webhooks/pkg"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
var _ = Context("Retries", func() {
|
|
var (
|
|
db = pgtesting.UsePostgresDatabase(pgServer)
|
|
ctx = logging.TestingContext()
|
|
srv = testserver.NewTestServer(func() testserver.Configuration {
|
|
return testserver.Configuration{
|
|
Postgres: db.GetValue().ConnectionOptions(),
|
|
Topics: []string{
|
|
"foo",
|
|
},
|
|
Debug: debug,
|
|
Output: GinkgoWriter,
|
|
NatsURL: natsServer.GetValue().URL,
|
|
RetryPeriod: time.Second,
|
|
MinBackoffDelay: time.Second,
|
|
AbortAfter: 3 * time.Second,
|
|
}
|
|
})
|
|
)
|
|
Context("the endpoint only returning transient errors", func() {
|
|
var httpServer *httptest.Server
|
|
BeforeEach(func() {
|
|
httpServer = httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "error", http.StatusInternalServerError)
|
|
}))
|
|
DeferCleanup(httpServer.Close)
|
|
|
|
cfg := components.ConfigUser{
|
|
Endpoint: httpServer.URL,
|
|
EventTypes: []string{
|
|
"foo",
|
|
},
|
|
}
|
|
_, err := srv.GetValue().Client().Webhooks.V1.InsertConfig(
|
|
ctx,
|
|
cfg,
|
|
)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
It("with an exponential backoff, 3 attempts have to be made and all should have a failed status", func() {
|
|
_, err := srv.GetValue().Client().Webhooks.V1.InsertConfig(
|
|
ctx,
|
|
components.ConfigUser{
|
|
Endpoint: httpServer.URL,
|
|
EventTypes: []string{
|
|
"foo",
|
|
},
|
|
},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
db, err := bunconnect.OpenSQLDB(logging.TestingContext(), db.GetValue().ConnectionOptions())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = natsServer.
|
|
GetValue().
|
|
Client(GinkgoT()).
|
|
Publish("foo", []byte(`{"type":"foo"}`))
|
|
Expect(err).To(BeNil())
|
|
|
|
Eventually(getNumAttemptsToRetry).WithArguments(db).
|
|
WithTimeout(5 * time.Second).
|
|
Should(BeNumerically(">", 0))
|
|
|
|
Eventually(getNumFailedAttempts).WithArguments(db).
|
|
WithTimeout(12 * time.Second).
|
|
Should(BeNumerically(">=", 3))
|
|
|
|
Eventually(getNumPendingRetryAttemptsForEndpoint).WithArguments(db, httpServer.URL).
|
|
WithTimeout(10 * time.Second).
|
|
Should(Equal(0))
|
|
})
|
|
})
|
|
Context("the endpoint recovering after a transient error", func() {
|
|
var (
|
|
httpServer *httptest.Server
|
|
recovered atomic.Bool
|
|
calls atomic.Int32
|
|
)
|
|
BeforeEach(func() {
|
|
recovered.Store(false)
|
|
calls.Store(0)
|
|
httpServer = httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
calls.Add(1)
|
|
if !recovered.Load() {
|
|
http.Error(w, "temporary error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
DeferCleanup(httpServer.Close)
|
|
|
|
cfg := components.ConfigUser{
|
|
Endpoint: httpServer.URL,
|
|
EventTypes: []string{
|
|
"foo",
|
|
},
|
|
}
|
|
_, err := srv.GetValue().Client().Webhooks.V1.InsertConfig(
|
|
ctx,
|
|
cfg,
|
|
)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
It("should deliver queued messages once the endpoint returns 200", func() {
|
|
db, err := bunconnect.OpenSQLDB(logging.TestingContext(), db.GetValue().ConnectionOptions())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = natsServer.
|
|
GetValue().
|
|
Client(GinkgoT()).
|
|
Publish("foo", []byte(`{"type":"foo"}`))
|
|
Expect(err).To(BeNil())
|
|
|
|
Eventually(getNumEndpointAttemptsByStatus).WithArguments(db, httpServer.URL, webhooks.StatusAttemptToRetry).
|
|
WithTimeout(5 * time.Second).
|
|
Should(BeNumerically(">", 0))
|
|
|
|
recovered.Store(true)
|
|
|
|
Eventually(getNumEndpointAttemptsByStatus).WithArguments(db, httpServer.URL, webhooks.StatusAttemptSuccess).
|
|
WithTimeout(10 * time.Second).
|
|
Should(BeNumerically(">", 0))
|
|
|
|
Eventually(getNumPendingRetryAttemptsForEndpoint).WithArguments(db, httpServer.URL).
|
|
WithTimeout(5 * time.Second).
|
|
Should(Equal(0))
|
|
|
|
Eventually(func() int32 {
|
|
return calls.Load()
|
|
}).WithTimeout(5 * time.Second).Should(BeNumerically(">=", 2))
|
|
})
|
|
})
|
|
Context("the endpoint returning a permanent client error", func() {
|
|
var httpServer *httptest.Server
|
|
BeforeEach(func() {
|
|
httpServer = httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "error", http.StatusNotFound)
|
|
}))
|
|
DeferCleanup(httpServer.Close)
|
|
|
|
cfg := components.ConfigUser{
|
|
Endpoint: httpServer.URL,
|
|
EventTypes: []string{
|
|
"foo",
|
|
},
|
|
}
|
|
_, err := srv.GetValue().Client().Webhooks.V1.InsertConfig(
|
|
ctx,
|
|
cfg,
|
|
)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
It("should fail immediately without scheduling any retry", func() {
|
|
db, err := bunconnect.OpenSQLDB(logging.TestingContext(), db.GetValue().ConnectionOptions())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = natsServer.
|
|
GetValue().
|
|
Client(GinkgoT()).
|
|
Publish("foo", []byte(`{"type":"foo"}`))
|
|
Expect(err).To(BeNil())
|
|
|
|
Eventually(getNumEndpointAttemptsByStatus).WithArguments(db, httpServer.URL, webhooks.StatusAttemptFailed).
|
|
WithTimeout(5 * time.Second).
|
|
Should(BeNumerically(">=", 1))
|
|
|
|
// A permanent 4xx must never enter the retry queue
|
|
Consistently(getNumPendingRetryAttemptsForEndpoint).WithArguments(db, httpServer.URL).
|
|
WithTimeout(3 * time.Second).
|
|
Should(Equal(0))
|
|
})
|
|
})
|
|
})
|
|
|
|
func getNumAttemptsToRetry(db *bun.DB) (int, error) {
|
|
var results []webhooks.Attempt
|
|
err := db.NewSelect().Model(&results).
|
|
Where("status = ?", "to retry").
|
|
Scan(logging.TestingContext())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(results), nil
|
|
}
|
|
|
|
func getNumFailedAttempts(db *bun.DB) (int, error) {
|
|
var results []webhooks.Attempt
|
|
err := db.NewSelect().Model(&results).
|
|
Where("status = ?", "failed").
|
|
Scan(logging.TestingContext())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(results), nil
|
|
}
|
|
|
|
func getNumEndpointAttemptsByStatus(db *bun.DB, endpoint, status string) (int, error) {
|
|
count, err := db.NewSelect().Model((*webhooks.Attempt)(nil)).
|
|
Where("config->>'endpoint' = ?", endpoint).
|
|
Where("status = ?", status).
|
|
Count(logging.TestingContext())
|
|
return count, err
|
|
}
|
|
|
|
func getNumPendingRetryAttemptsForEndpoint(db *bun.DB, endpoint string) (int, error) {
|
|
count, err := db.NewSelect().Model((*webhooks.Attempt)(nil)).
|
|
Where("config->>'endpoint' = ?", endpoint).
|
|
Where("status IN (?)", bun.List([]string{
|
|
webhooks.StatusAttemptToRetry,
|
|
webhooks.StatusAttemptRetrying,
|
|
})).
|
|
Count(logging.TestingContext())
|
|
return count, err
|
|
}
|