110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func setupInstanceGuardRouter(expectedID string) *gin.Engine {
|
|
r := gin.New()
|
|
r.Use(instanceIDGuard(expectedID))
|
|
r.GET("/api/v1/data", func(c *gin.Context) { c.JSON(200, gin.H{"data": "secret"}) })
|
|
return r
|
|
}
|
|
|
|
// TestInstanceIDGuard_EmptyExpectedID_NoOp covers the legacy / test wiring
|
|
// path where InstanceConfig.InstanceID is unset. The guard must let every
|
|
// request through regardless of X-Instance-ID so callers that predate this
|
|
// middleware keep working.
|
|
func TestInstanceIDGuard_EmptyExpectedID_NoOp(t *testing.T) {
|
|
r := setupInstanceGuardRouter("")
|
|
|
|
tests := []struct {
|
|
name string
|
|
header string
|
|
}{
|
|
{"no header", ""},
|
|
{"matching header", "anything"},
|
|
{"mismatched header", "different"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/v1/data", nil)
|
|
if tt.header != "" {
|
|
req.Header.Set(InstanceIDHeader, tt.header)
|
|
}
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 (no-op when expectedID empty), got %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInstanceIDGuard_MissingHeader_Passes covers the backward-compat
|
|
// case: the agent subprocess's local MCP loopback, raw curl probes, and
|
|
// older clients don't send X-Instance-ID. Missing header must pass —
|
|
// only an *explicit mismatch* is a stale-client signal.
|
|
func TestInstanceIDGuard_MissingHeader_Passes(t *testing.T) {
|
|
r := setupInstanceGuardRouter("instance-abc")
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/v1/data", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 (missing header is allowed), got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestInstanceIDGuard_MatchingHeader_Passes is the normal path —
|
|
// kandev's agentctl Client stamps the executionID it owns and gets
|
|
// through to its own instance's handlers.
|
|
func TestInstanceIDGuard_MatchingHeader_Passes(t *testing.T) {
|
|
r := setupInstanceGuardRouter("instance-abc")
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/v1/data", nil)
|
|
req.Header.Set(InstanceIDHeader, "instance-abc")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for matching header, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestInstanceIDGuard_MismatchedHeader_404 is the bug-fix path: a
|
|
// stale client whose port was recycled to a different instance must
|
|
// be rejected with 404 instead of accidentally driving the new
|
|
// instance's process Manager.
|
|
func TestInstanceIDGuard_MismatchedHeader_404(t *testing.T) {
|
|
r := setupInstanceGuardRouter("instance-abc")
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/v1/data", nil)
|
|
req.Header.Set(InstanceIDHeader, "instance-xyz")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 on mismatch, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestInstanceIDGuard_TrimsWhitespace defends against a client that
|
|
// accidentally appends a newline or spaces to the header value (a
|
|
// past source of "why doesn't my header match" bugs in middleware
|
|
// that uses raw Header.Get).
|
|
func TestInstanceIDGuard_TrimsWhitespace(t *testing.T) {
|
|
r := setupInstanceGuardRouter("instance-abc")
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/api/v1/data", nil)
|
|
req.Header.Set(InstanceIDHeader, " instance-abc ")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 after trim, got %d", w.Code)
|
|
}
|
|
}
|