189 lines
6.2 KiB
Go
189 lines
6.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/servertiming"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func runServerTimingRequest(
|
|
t *testing.T,
|
|
enabled bool,
|
|
path string,
|
|
marker string,
|
|
role string,
|
|
handler gin.HandlerFunc,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
engine := gin.New()
|
|
engine.Use(ServerTiming(enabled))
|
|
engine.Any("/*path", func(c *gin.Context) {
|
|
if role != "" {
|
|
c.Set(string(ContextKeyUserRole), role)
|
|
}
|
|
handler(c)
|
|
})
|
|
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
if marker != "" {
|
|
request.Header.Set(servertiming.AdminUIHeader, marker)
|
|
}
|
|
engine.ServeHTTP(recorder, request)
|
|
return recorder
|
|
}
|
|
|
|
func TestServerTimingScopesAndRoleGate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enabled bool
|
|
path string
|
|
marker string
|
|
role string
|
|
wantHeader bool
|
|
}{
|
|
{name: "disabled", enabled: false, path: "/api/v1/admin/users", role: "admin"},
|
|
{name: "admin API path", enabled: true, path: "/api/v1/admin/users", role: "admin", wantHeader: true},
|
|
{name: "shared API marked by admin UI", enabled: true, path: "/api/v1/groups/available", marker: "1", role: "admin", wantHeader: true},
|
|
{name: "non admin role", enabled: true, path: "/api/v1/groups/available", marker: "1", role: "user"},
|
|
{name: "unauthenticated public request", enabled: true, path: "/api/v1/settings/public", marker: "1"},
|
|
{name: "unmarked shared API", enabled: true, path: "/api/v1/groups/available", role: "admin"},
|
|
{name: "invalid marker", enabled: true, path: "/api/v1/groups/available", marker: "true", role: "admin"},
|
|
{name: "admin prefix boundary", enabled: true, path: "/api/v1/administrator", role: "admin"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
recorder := runServerTimingRequest(t, tt.enabled, tt.path, tt.marker, tt.role, func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
})
|
|
header := recorder.Header().Get(servertiming.HeaderName)
|
|
if tt.wantHeader && header == "" {
|
|
t.Fatalf("%s header missing", servertiming.HeaderName)
|
|
}
|
|
if !tt.wantHeader && header != "" {
|
|
t.Fatalf("unexpected %s header: %q", servertiming.HeaderName, header)
|
|
}
|
|
if header != "" && (!strings.Contains(header, "total;dur=") || !strings.Contains(header, `cache;desc="bypass"`)) {
|
|
t.Fatalf("incomplete timing header: %q", header)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerTimingCollectorIsRequestScoped(t *testing.T) {
|
|
active := false
|
|
recorder := runServerTimingRequest(t, true, "/api/v1/keys", "1", "admin", func(c *gin.Context) {
|
|
active = servertiming.Active(c.Request.Context())
|
|
c.Status(http.StatusNoContent)
|
|
})
|
|
if !active {
|
|
t.Fatal("collector was not attached to marked request context")
|
|
}
|
|
if recorder.Header().Get(servertiming.HeaderName) == "" {
|
|
t.Fatal("timing header missing from status-only response")
|
|
}
|
|
}
|
|
|
|
func TestServerTimingFinalizesBeforeEarlyCommit(t *testing.T) {
|
|
recorder := runServerTimingRequest(t, true, "/api/v1/admin/stream", "", "admin", func(c *gin.Context) {
|
|
c.Status(http.StatusAccepted)
|
|
c.Writer.WriteHeaderNow()
|
|
})
|
|
if got := recorder.Header().Get(servertiming.HeaderName); got == "" {
|
|
t.Fatal("timing header was not written before response commit")
|
|
}
|
|
}
|
|
|
|
func TestServerTimingFinalizesOnFlush(t *testing.T) {
|
|
recorder := runServerTimingRequest(t, true, "/api/v1/admin/export", "", "admin", func(c *gin.Context) {
|
|
c.Writer.Flush()
|
|
})
|
|
if got := recorder.Header().Get(servertiming.HeaderName); got == "" {
|
|
t.Fatal("timing header was not written before stream flush")
|
|
}
|
|
}
|
|
|
|
func TestServerTimingStatusResponses(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status int
|
|
}{
|
|
{name: "not modified", status: http.StatusNotModified},
|
|
{name: "internal error", status: http.StatusInternalServerError},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
recorder := runServerTimingRequest(t, true, "/api/v1/admin/test", "", "admin", func(c *gin.Context) {
|
|
c.Status(tt.status)
|
|
})
|
|
if recorder.Code != tt.status {
|
|
t.Fatalf("status = %d, want %d", recorder.Code, tt.status)
|
|
}
|
|
if got := recorder.Header().Get(servertiming.HeaderName); got == "" {
|
|
t.Fatalf("timing header missing from status %d response", tt.status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerTimingResponseWriterUnwraps(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
baseWriter := c.Writer
|
|
writer := &serverTimingResponseWriter{ResponseWriter: baseWriter}
|
|
if got := writer.Unwrap(); got != baseWriter {
|
|
t.Fatalf("Unwrap() = %T, want original Gin writer", got)
|
|
}
|
|
}
|
|
|
|
func TestServerTimingCacheOutcome(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
headerName string
|
|
value string
|
|
want string
|
|
}{
|
|
{name: "snapshot hit", headerName: snapshotCacheHeader, value: "hit", want: "hit"},
|
|
{name: "usage miss", headerName: usageCacheHeader, value: "MISS", want: "miss"},
|
|
{name: "invalid", headerName: snapshotCacheHeader, value: "stale", want: "bypass"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
recorder := runServerTimingRequest(t, true, "/api/v1/admin/dashboard", "", "admin", func(c *gin.Context) {
|
|
c.Header(tt.headerName, tt.value)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
})
|
|
want := `cache;desc="` + tt.want + `"`
|
|
if got := recorder.Header().Get(servertiming.HeaderName); !strings.Contains(got, want) {
|
|
t.Fatalf("timing header %q does not contain %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerTimingResponseHeaderForWebSocket(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops/ws/qps", nil)
|
|
collector := servertiming.New(time.Now())
|
|
c.Request = c.Request.WithContext(servertiming.WithCollector(c.Request.Context(), collector))
|
|
c.Set(string(ContextKeyUserRole), "admin")
|
|
|
|
header := ServerTimingResponseHeader(c)
|
|
if header.Get(servertiming.HeaderName) == "" {
|
|
t.Fatal("WebSocket response header missing timing value")
|
|
}
|
|
|
|
c.Set(string(ContextKeyUserRole), "user")
|
|
if got := ServerTimingResponseHeader(c); got != nil {
|
|
t.Fatalf("non-admin WebSocket received timing header: %#v", got)
|
|
}
|
|
}
|