Add webhook logging, pagination, middleware, migrations, and prod hardening
- Webhook event logging repo and subscription webhook idempotency - Pagination helper (echohelpers) with cursor/offset support - Request ID and structured logging middleware - Push client improvements (FCM HTTP v1, better error handling) - Task model version column, business constraint migrations, targeted indexes - Expanded categorization chain tests - Email service and config hardening - CI workflow updates, .gitignore additions, .env.example updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
32
internal/echohelpers/pagination.go
Normal file
32
internal/echohelpers/pagination.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package echohelpers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// ParsePagination extracts limit and offset from query parameters with bounded defaults.
|
||||
// maxLimit caps the maximum page size to prevent unbounded queries.
|
||||
func ParsePagination(c echo.Context, maxLimit int) (limit, offset int) {
|
||||
const defaultLimit = 50
|
||||
|
||||
limit = defaultLimit
|
||||
if l := c.QueryParam("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
if limit > maxLimit {
|
||||
limit = maxLimit
|
||||
}
|
||||
|
||||
offset = 0
|
||||
if o := c.QueryParam("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return limit, offset
|
||||
}
|
||||
77
internal/echohelpers/pagination_test.go
Normal file
77
internal/echohelpers/pagination_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package echohelpers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParsePagination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
maxLimit int
|
||||
expectedLimit int
|
||||
expectedOffset int
|
||||
}{
|
||||
{
|
||||
name: "Defaults - no query params",
|
||||
query: "/",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "Custom values",
|
||||
query: "/?limit=20&offset=10",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 20,
|
||||
expectedOffset: 10,
|
||||
},
|
||||
{
|
||||
name: "Max limit capped",
|
||||
query: "/?limit=500",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 200,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "Negative offset ignored",
|
||||
query: "/?offset=-5",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "Invalid limit falls back to default",
|
||||
query: "/?limit=abc",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "Zero limit falls back to default",
|
||||
query: "/?limit=0",
|
||||
maxLimit: 200,
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodGet, tt.query, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
limit, offset := ParsePagination(c, tt.maxLimit)
|
||||
|
||||
assert.Equal(t, tt.expectedLimit, limit, "limit mismatch")
|
||||
assert.Equal(t, tt.expectedOffset, offset, "offset mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user