mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 00:01:22 +00:00
feat(api): error codes, idempotency keys, cursor pagination
- Stable machine-readable error codes alongside the error message (UNAUTHENTICATED, INVALID_TOKEN, ADMIN_REQUIRED, SCOPE_INSUFFICIENT, IDEMPOTENCY_IN_PROGRESS); CLI renders "CODE: message". - Idempotency middleware on authed/admin groups: POST requests with an Idempotency-Key header replay the first response for 24h (Redis-backed, caller-scoped); concurrent duplicates get 409. X-Idempotent-Replay header marks replays. - Shared pagination helpers: ?cursor= (opaque, preferred) alongside legacy ?page= on projects and deployments lists; pagination meta includes next_cursor; invalid cursors fail soft to page 1. - CLI --idempotency-key flag for safe scripted retries. - OpenAPI updated: error envelope, cursor params, idempotency contract. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
485e41a817
commit
18f78f00a3
15 files changed
+454
-56
No files matched your search
@@ -0,0 +1,60 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestIdempotencyPassThroughWithoutKey(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
called := 0
|
||||
router := gin.New()
|
||||
router.Use(Idempotency())
|
||||
router.POST("/x", func(c *gin.Context) { called++; c.JSON(200, gin.H{"ok": true}) })
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/x", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if called != 1 || rec.Code != 200 {
|
||||
t.Fatalf("expected pass-through, called=%d code=%d", called, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdempotencyPassThroughWithoutRedis(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
called := 0
|
||||
router := gin.New()
|
||||
router.Use(Idempotency())
|
||||
router.POST("/x", func(c *gin.Context) { called++; c.JSON(200, gin.H{"ok": true}) })
|
||||
|
||||
// Key present but no redis in context — must not block the request.
|
||||
req := httptest.NewRequest(http.MethodPost, "/x", nil)
|
||||
req.Header.Set("Idempotency-Key", "abc-123")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if called != 1 || rec.Code != 200 {
|
||||
t.Fatalf("expected pass-through without redis, called=%d code=%d", called, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdempotencySkipsNonPOST(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
called := 0
|
||||
router := gin.New()
|
||||
router.Use(Idempotency())
|
||||
router.GET("/x", func(c *gin.Context) { called++; c.String(200, "ok") })
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
req.Header.Set("Idempotency-Key", "abc")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if called != 1 {
|
||||
t.Fatalf("GET should bypass idempotency, called=%d", called)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user