Files
Agent-System/internal/engineeringdashboard/server_test.go
T
weichenandCursor 7fd7fb8a2a fix(engineering-dashboard): read token from URL/meta under CSP
Remove the inline token script blocked by script-src 'self', so the
live page no longer calls the snapshot API with token=undefined.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 07:39:21 +00:00

171 lines
5.3 KiB
Go

package engineeringdashboard
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestServer_RejectsNonGETAndBadHost(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{Map: map[string]FakeResult{}})
c.mu.Lock()
c.current = &Snapshot{
SchemaVersion: SchemaVersion, GenerationID: "gen_1", RevisionID: "rev_1",
GeneratedAt: time.Now().UTC(), Repository: RepositoryIdentity{Root: "/r"},
}
c.mu.Unlock()
srv, err := NewServer(c)
if err != nil {
t.Fatal(err)
}
h := srv.Handler()
req := httptest.NewRequest(http.MethodPost, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("POST: %d", rr.Code)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "evil.example"
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("bad host: %d", rr.Code)
}
}
func TestServer_TokenAndSnapshotIdentity(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{Map: map[string]FakeResult{}})
snap := Snapshot{
SchemaVersion: SchemaVersion, GenerationID: "gen_9", RevisionID: "rev_abc",
GeneratedAt: time.Now().UTC(), Repository: RepositoryIdentity{Root: "/repo", Branch: "dev", Head: "h"},
Tasks: []TaskState{SampleRDevTaskFixture()},
}
c.mu.Lock()
c.current = &snap
c.mu.Unlock()
srv, err := NewServer(c)
if err != nil {
t.Fatal(err)
}
h := srv.Handler()
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("no token: %d", rr.Code)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != 200 {
t.Fatalf("snapshot: %d %s", rr.Code, rr.Body.String())
}
body := rr.Body.String()
if !strings.Contains(body, `"generation_id": "gen_9"`) || !strings.Contains(body, `"revision_id": "rev_abc"`) {
t.Fatalf("identity missing: %s", body)
}
if rr.Header().Get("ETag") != `"rev_abc"` {
t.Fatalf("etag %q", rr.Header().Get("ETag"))
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
req.Header.Set("If-None-Match", `"rev_abc"`)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusNotModified {
t.Fatalf("etag: %d", rr.Code)
}
}
func TestServer_DocPathTraversalDenied(t *testing.T) {
root := t.TempDir()
writeDoc(t, root, ".doc/ops/ok.md", "# ok\n")
c := NewCollector(root, &FakeRunner{Map: map[string]FakeResult{}})
srv, err := NewServer(c)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/api/v1/doc/not-a-real.md?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
req.URL.Path = "/api/v1/doc/../../.env"
rr := httptest.NewRecorder()
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("got %d body=%s", rr.Code, rr.Body.String())
}
}
func TestListenAndServe_RejectsNonLoopback(t *testing.T) {
err := ListenAndServe("0.0.0.0:8787", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
if err == nil || !strings.Contains(err.Error(), "loopback") {
t.Fatalf("expected loopback reject, got %v", err)
}
}
func TestServer_CrossOriginDenied(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{})
c.mu.Lock()
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
c.mu.Unlock()
srv, _ := NewServer(c)
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
req.Header.Set("Origin", "https://evil.example")
rr := httptest.NewRecorder()
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("got %d", rr.Code)
}
}
func TestServer_IndexUsesMetaTokenNoInlineScript(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{})
c.mu.Lock()
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
c.mu.Unlock()
srv, err := NewServer(c)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("index: %d %s", rr.Code, rr.Body.String())
}
body := rr.Body.String()
if strings.Contains(body, "window.__DASHBOARD_TOKEN__") {
t.Fatal("index must not use inline token script (CSP script-src 'self')")
}
meta := `name="engineering-dashboard-token" content="` + srv.Token + `"`
if !strings.Contains(body, meta) {
t.Fatalf("missing token meta: %s", body[:min(400, len(body))])
}
if !strings.Contains(rr.Header().Get("Content-Security-Policy"), "script-src 'self'") {
t.Fatalf("unexpected CSP: %s", rr.Header().Get("Content-Security-Policy"))
}
// app.js must not reference the removed global
js, err := readAsset("assets/app.js")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(js), "__DASHBOARD_TOKEN__") {
t.Fatal("app.js still references __DASHBOARD_TOKEN__")
}
if !strings.Contains(string(js), "engineering-dashboard-token") {
t.Fatal("app.js should read meta engineering-dashboard-token")
}
}