Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/agent/internal/localapi/trigger_test.go
Fimeg 1241c1ef01 feat: embed web UI in server binary + local trigger-scan write endpoint
Server becomes self-contained: web/dist embedded via go:embed
(server/internal/webui), SPA served from the binary with JSON-404 guard on
/api paths, nginx web container removed from compose (31336 now maps to the
server). Clean checkouts without the UI copy build API-only.

Agent local API gains its first write endpoint, POST /v1/actions/trigger-scan
(FEAT-002 write path): group-ACL authorized, single-flight, 202/409/503
semantics. Registered agents run the same HandleScanUpdates path as a signed
scan command (empty command_id, no ack tracking); standalone agents scan
through the orchestrator into the local read model only. Also repairs
localapi tests left uncompilable by the desktop-provider parameter.
2026-06-10 08:38:18 -04:00

88 lines
2.5 KiB
Go

package localapi
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/Fimeg/RedFlag/agent/internal/cache"
)
func triggerHandler(t *testing.T, trigger func(source string) error) http.Handler {
t.Helper()
return newHandler(testConfig(t), func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
}, nil, trigger)
}
func postTrigger(t *testing.T, handler http.Handler) *httptest.ResponseRecorder {
t.Helper()
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/v1/actions/trigger-scan", nil))
return rec
}
func TestTriggerScanAccepted(t *testing.T) {
var gotSource string
handler := triggerHandler(t, func(source string) error {
gotSource = source
return nil
})
rec := postTrigger(t, handler)
if rec.Code != http.StatusAccepted {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusAccepted, rec.Body.String())
}
if gotSource != "localapi" {
t.Fatalf("source = %q, want localapi", gotSource)
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp["accepted"] != true {
t.Fatalf("accepted = %v, want true", resp["accepted"])
}
}
func TestTriggerScanInFlightIsConflict(t *testing.T) {
handler := triggerHandler(t, func(string) error { return ErrScanInFlight })
rec := postTrigger(t, handler)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusConflict)
}
}
func TestTriggerScanUnavailableWithoutCallback(t *testing.T) {
handler := triggerHandler(t, nil)
rec := postTrigger(t, handler)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable)
}
}
func TestTriggerScanFailureIs500(t *testing.T) {
handler := triggerHandler(t, func(string) error { return errors.New("boom") })
rec := postTrigger(t, handler)
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
}
func TestTriggerScanRejectsGet(t *testing.T) {
handler := triggerHandler(t, func(string) error { return nil })
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/actions/trigger-scan", nil))
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
}
if rec.Header().Get("Allow") != http.MethodPost {
t.Fatalf("Allow = %q, want POST", rec.Header().Get("Allow"))
}
}