Files
webhooker/internal/handlers/handlers_test.go
clawbot 5e683af2a4
All checks were successful
check / check (push) Successful in 58s
refactor: event-driven delivery engine with channel notifications and timer-based retries
Replace the polling-based delivery engine with a fully event-driven
architecture using Go channels and goroutines:

- Webhook handler notifies engine via buffered channel after creating
  delivery records, with inline event data for payloads < 16KB
- Large payloads (>= 16KB) use pointer semantics (Body *string = nil)
  and are fetched from DB on demand, keeping channel memory bounded
- Failed retry-target deliveries schedule Go timers with exponential
  backoff; timers fire into a separate retry channel when ready
- On startup, engine scans DB once to recover interrupted deliveries
  (pending processed immediately, retrying get timers for remaining
  backoff)
- DB stores delivery status for crash recovery only, not for
  inter-component communication during normal operation
- delivery.Notifier interface decouples handlers from engine; fx wires
  *Engine as Notifier

No more periodic polling. No more wasted cycles when idle.
2026-03-01 21:46:16 -08:00

144 lines
3.3 KiB
Go

package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/database"
"sneak.berlin/go/webhooker/internal/delivery"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/healthcheck"
"sneak.berlin/go/webhooker/internal/logger"
"sneak.berlin/go/webhooker/internal/session"
)
// noopNotifier is a no-op delivery.Notifier for tests.
type noopNotifier struct{}
func (n *noopNotifier) Notify(delivery.Notification) {}
func TestHandleIndex(t *testing.T) {
var h *Handlers
app := fxtest.New(
t,
fx.Provide(
globals.New,
logger.New,
func() *config.Config {
return &config.Config{
// This is a base64 encoded 32-byte key: "test-session-key-32-bytes-long!!"
SessionKey: "dGVzdC1zZXNzaW9uLWtleS0zMi1ieXRlcy1sb25nISE=",
DataDir: t.TempDir(),
}
},
func() *database.Database {
// Mock database with a mock DB method
db := &database.Database{}
return db
},
database.NewWebhookDBManager,
healthcheck.New,
session.New,
func() delivery.Notifier { return &noopNotifier{} },
New,
),
fx.Populate(&h),
)
app.RequireStart()
defer app.RequireStop()
// Since we can't test actual template rendering without templates,
// let's test that the handler is created and doesn't panic
handler := h.HandleIndex()
assert.NotNil(t, handler)
}
func TestRenderTemplate(t *testing.T) {
var h *Handlers
app := fxtest.New(
t,
fx.Provide(
globals.New,
logger.New,
func() *config.Config {
return &config.Config{
// This is a base64 encoded 32-byte key: "test-session-key-32-bytes-long!!"
SessionKey: "dGVzdC1zZXNzaW9uLWtleS0zMi1ieXRlcy1sb25nISE=",
DataDir: t.TempDir(),
}
},
func() *database.Database {
// Mock database
return &database.Database{}
},
database.NewWebhookDBManager,
healthcheck.New,
session.New,
func() delivery.Notifier { return &noopNotifier{} },
New,
),
fx.Populate(&h),
)
app.RequireStart()
defer app.RequireStop()
t.Run("handles missing templates gracefully", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
data := map[string]interface{}{
"Version": "1.0.0",
}
// When a non-existent template name is requested, renderTemplate
// should return an internal server error
h.renderTemplate(w, req, "nonexistent.html", data)
// Should return internal server error when template is not found
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
}
func TestFormatUptime(t *testing.T) {
tests := []struct {
name string
duration string
expected string
}{
{
name: "minutes only",
duration: "45m",
expected: "45m",
},
{
name: "hours and minutes",
duration: "2h30m",
expected: "2h 30m",
},
{
name: "days, hours and minutes",
duration: "25h45m",
expected: "1d 1h 45m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d, err := time.ParseDuration(tt.duration)
require.NoError(t, err)
result := formatUptime(d)
assert.Equal(t, tt.expected, result)
})
}
}