Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/agent/internal/localapi/server_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

184 lines
5.2 KiB
Go

package localapi
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Fimeg/RedFlag/agent/internal/cache"
"github.com/Fimeg/RedFlag/agent/internal/client"
"github.com/Fimeg/RedFlag/agent/internal/config"
"github.com/gofrs/uuid/v5"
)
func TestIdentityRedactsSecrets(t *testing.T) {
cfg := testConfig(t)
handler := newHandler(cfg, func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
}, nil, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/identity", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
body := rec.Body.String()
for _, forbidden := range []string{"secret-access-token", "secret-refresh-token", "registration-token", "refresh_token", "token"} {
if strings.Contains(body, forbidden) {
t.Fatalf("identity response leaked %q: %s", forbidden, body)
}
}
var resp IdentityResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.AgentID != cfg.AgentID.String() {
t.Fatalf("agent_id = %q, want %q", resp.AgentID, cfg.AgentID.String())
}
if resp.Registered != true {
t.Fatalf("registered = false, want true")
}
if got := strings.Join(resp.Tags, ","); got != "alpha,zeta" {
t.Fatalf("tags = %q, want sorted alpha,zeta", got)
}
}
func TestStatusAndPackagesReadLocalCache(t *testing.T) {
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
localCache := &cache.LocalCache{
LastScanTime: now.Add(-2 * time.Minute),
LastCheckIn: now.Add(-1 * time.Minute),
LastUpdated: now,
UpdateCount: 1,
AgentStatus: "online",
Summary: cache.UpdateSummary{
Total: 1,
ByEcosystem: map[string]int{"windows": 1},
BySeverity: map[string]int{"critical": 1},
},
Scanners: map[string]cache.ScannerState{
"windows": {
Name: "windows",
Status: "success",
UpdateCount: 1,
},
},
Updates: []client.UpdateReportItem{
{
PackageType: "windows",
PackageName: "KB5000001",
CurrentVersion: "1",
AvailableVersion: "2",
Severity: "critical",
},
},
Capabilities: cache.CapabilityTokenState{
PendingCount: 2,
LastFetchedCount: 3,
LastProcessedCount: 1,
},
}
handler := newHandler(testConfig(t), func() (*cache.LocalCache, error) {
return localCache, nil
}, nil, nil)
var status StatusResponse
requestJSON(t, handler, http.MethodGet, "/v1/status", &status)
if status.AgentStatus != "online" {
t.Fatalf("agent_status = %q, want online", status.AgentStatus)
}
if status.Summary.Total != 1 {
t.Fatalf("summary.total = %d, want 1", status.Summary.Total)
}
var packages ScanResponse
requestJSON(t, handler, http.MethodGet, "/v1/packages", &packages)
if len(packages.Updates) != 1 {
t.Fatalf("updates len = %d, want 1", len(packages.Updates))
}
if packages.Updates[0].PackageName != "KB5000001" {
t.Fatalf("package name = %q, want KB5000001", packages.Updates[0].PackageName)
}
var tokens TokenResponse
requestJSON(t, handler, http.MethodGet, "/v1/tokens/active", &tokens)
if tokens.Capabilities.PendingCount != 2 {
t.Fatalf("pending tokens = %d, want 2", tokens.Capabilities.PendingCount)
}
}
func TestCacheLoadFailureReturnsServiceUnavailable(t *testing.T) {
handler := newHandler(testConfig(t), func() (*cache.LocalCache, error) {
return nil, errors.New("cannot read cache")
}, nil, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable)
}
}
func TestOnlyGetMethodsAllowed(t *testing.T) {
handler := newHandler(testConfig(t), func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
}, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
}
if rec.Header().Get("Allow") != http.MethodGet {
t.Fatalf("Allow = %q, want GET", rec.Header().Get("Allow"))
}
}
func requestJSON(t *testing.T, handler http.Handler, method, path string, target interface{}) {
t.Helper()
req := httptest.NewRequest(method, path, nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s %s status = %d, want %d; body=%s", method, path, rec.Code, http.StatusOK, rec.Body.String())
}
if err := json.Unmarshal(rec.Body.Bytes(), target); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
}
func testConfig(t *testing.T) *config.Config {
t.Helper()
agentID, err := uuid.NewV4()
if err != nil {
t.Fatalf("new uuid: %v", err)
}
return &config.Config{
Version: "5",
ServerURL: "https://redflag.example",
RegistrationToken: "registration-token",
AgentID: agentID,
Token: "secret-access-token",
RefreshToken: "secret-refresh-token",
CheckInInterval: 300,
Tags: []string{"zeta", "alpha"},
DisplayName: "workstation-01",
Organization: "lab",
OSType: "windows",
}
}