agent-orchestrator/backend/internal/httpd/control_test.go

53 lines
1.6 KiB
Go

package httpd
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/aoagents/agent-orchestrator/backend/internal/config"
)
// TestShutdownGuard verifies that POST /shutdown only fires for a trusted local
// caller: a loopback Host with no Origin header. A cross-site Origin or a
// non-loopback (DNS-rebinding) Host must be rejected without triggering the
// shutdown side effect.
func TestShutdownGuard(t *testing.T) {
cases := []struct {
name string
host string
origin string
wantStatus int
wantFired bool
}{
{name: "loopback no origin", host: "127.0.0.1:3001", wantStatus: http.StatusAccepted, wantFired: true},
{name: "localhost no origin", host: "localhost:3001", wantStatus: http.StatusAccepted, wantFired: true},
{name: "cross-site origin", host: "127.0.0.1:3001", origin: "https://evil.example", wantStatus: http.StatusForbidden, wantFired: false},
{name: "rebinding host", host: "evil.example", wantStatus: http.StatusForbidden, wantFired: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fired := false
r := NewRouterWithControl(config.Config{}, discardLogger(), nil, APIDeps{}, ControlDeps{
RequestShutdown: func() { fired = true },
})
req := httptest.NewRequest(http.MethodPost, "http://"+tc.host+"/shutdown", nil)
req.Host = tc.host
if tc.origin != "" {
req.Header.Set("Origin", tc.origin)
}
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != tc.wantStatus {
t.Fatalf("status = %d, want %d", rec.Code, tc.wantStatus)
}
if fired != tc.wantFired {
t.Fatalf("shutdown fired = %v, want %v", fired, tc.wantFired)
}
})
}
}