Files
gadfly-reports/server_test.go
T
steve ddcf42a3ce
Build & push image / build-and-push (push) Successful in 1m13s
CI / test (push) Successful in 10m39s
feat: gadfly-reports — findings store + scoreboard daemon
SQLite-backed HTTP store for Gadfly review findings, per-review run timings, and human/Claude grades, with a points-free per-model scoreboard. Pure fact store: it computes no points or rankings (the dashboard maps severity->points client-side and retunes without re-scoring). Findings are content-addressed by location so cross-model reports collapse for consensus; one grade per finding, latest wins. Pure-Go SQLite (CGO-free) + Docker image CI + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:55:24 -04:00

101 lines
3.0 KiB
Go

package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
)
func testServer(t *testing.T, token string) *httptest.Server {
t.Helper()
store, err := Open(filepath.Join(t.TempDir(), "gadfly-reports.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { store.Close() })
srv := httptest.NewServer(newServer(store, token))
t.Cleanup(srv.Close)
return srv
}
func post(t *testing.T, srv *httptest.Server, token, path string, body any) *http.Response {
t.Helper()
b, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", srv.URL+path, bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("POST %s: %v", path, err)
}
return resp
}
// TestServerEndToEnd: run -> reports -> grade -> scoreboard over HTTP.
func TestServerEndToEnd(t *testing.T) {
srv := testServer(t, "")
if resp := post(t, srv, "", "/runs", Run{RunID: "r1", Repo: "r", PR: 1, Model: "m", Provider: "p", DurationSecs: 120}); resp.StatusCode != 200 {
t.Fatalf("POST /runs = %d", resp.StatusCode)
}
resp := post(t, srv, "", "/reports", []ReportIn{
{Repo: "r", PR: 1, Lens: "security", File: "a.go", Line: 7, Title: "leak", Model: "m", Provider: "p", RunID: "r1"},
})
if resp.StatusCode != 200 {
t.Fatalf("POST /reports = %d", resp.StatusCode)
}
var rep struct {
FindingIDs []string `json:"finding_ids"`
}
json.NewDecoder(resp.Body).Decode(&rep)
if len(rep.FindingIDs) != 1 {
t.Fatalf("want 1 finding id, got %v", rep.FindingIDs)
}
id := rep.FindingIDs[0]
if resp := post(t, srv, "", "/findings/"+id+"/grade", Grade{IsReal: true, Severity: "medium", Grader: "claude"}); resp.StatusCode != 200 {
t.Fatalf("POST grade = %d", resp.StatusCode)
}
resp = mustGet(t, srv, "", "/scoreboard")
var board []ModelStat
json.NewDecoder(resp.Body).Decode(&board)
if len(board) != 1 || board[0].Confirmed != 1 || board[0].BySeverity["medium"] != 1 || board[0].Minutes != 2 {
t.Fatalf("unexpected scoreboard: %+v", board)
}
}
// TestServerAuth: a set token gates writes but leaves /healthz open.
func TestServerAuth(t *testing.T) {
srv := testServer(t, "secret")
if resp := post(t, srv, "", "/runs", Run{RunID: "r1", Model: "m"}); resp.StatusCode != http.StatusUnauthorized {
t.Errorf("unauthenticated POST = %d, want 401", resp.StatusCode)
}
if resp := post(t, srv, "secret", "/runs", Run{RunID: "r1", Model: "m"}); resp.StatusCode != 200 {
t.Errorf("authenticated POST = %d, want 200", resp.StatusCode)
}
if resp := mustGet(t, srv, "", "/healthz"); resp.StatusCode != 200 {
t.Errorf("healthz should be open, got %d", resp.StatusCode)
}
}
func mustGet(t *testing.T, srv *httptest.Server, token, path string) *http.Response {
t.Helper()
req, _ := http.NewRequest("GET", srv.URL+path, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
return resp
}