mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
- 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>
61 lines
1.6 KiB
Go
61 lines
1.6 KiB
Go
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)
|
|
}
|
|
}
|