Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/agent/internal/localapi/server_test.go
Fimeg f487de554a desktop: become the machine console
Qt/QML now carries 11 local views while the Agent owns observation and intent. Tauri, WebKit, and the second React desktop build leave together. Linux ships first; Windows waits for a native Qt runner.
2026-09-01 08:29:58 -04:00

334 lines
11 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/Fimeg/RedFlag/agent/internal/system"
"github.com/gofrs/uuid/v5"
)
func TestIdentityRedactsSecrets(t *testing.T) {
cfg := testConfig(t)
handler := newHandler(Options{Config: cfg, LoadCache: func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, 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(Options{Config: testConfig(t), LoadCache: func() (*cache.LocalCache, error) {
return localCache, 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 TestSystemHealthUsesAgentCollectors(t *testing.T) {
handler := newHandler(Options{
Config: testConfig(t),
LoadCache: func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
},
SystemProvider: func() (*system.SystemInfo, error) {
return &system.SystemInfo{
Hostname: "workstation-01",
OSVersion: "Arch Linux",
OSArchitecture: "amd64",
RunningProcesses: 173,
Uptime: "4 days",
MemoryInfo: system.MemoryInfo{
Total: 32 << 30,
Used: 12 << 30,
Available: 20 << 30,
UsedPercent: 37.5,
},
}, nil
},
ProcessProvider: func(limit int) ([]system.TopProcess, error) {
if limit != 8 {
t.Fatalf("process limit = %d, want 8", limit)
}
return []system.TopProcess{{Name: "redflag-agent", PID: 42, CPU: 1.5, Mem: 0.8}}, nil
},
})
var response SystemResponse
requestJSON(t, handler, http.MethodGet, "/v1/system", &response)
if response.System.Hostname != "workstation-01" {
t.Fatalf("hostname = %q, want workstation-01", response.System.Hostname)
}
if response.System.MemoryInfo.UsedPercent != 37.5 {
t.Fatalf("memory used = %.1f, want 37.5", response.System.MemoryInfo.UsedPercent)
}
if len(response.TopProcesses) != 1 || response.TopProcesses[0].Name != "redflag-agent" {
t.Fatalf("top processes = %#v", response.TopProcesses)
}
}
func TestMonitorAndProcessInventoryUseAgentCollectors(t *testing.T) {
now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
handler := newHandler(Options{
Config: testConfig(t),
LoadCache: func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
},
MonitorProvider: func() (*system.ResourceSnapshot, error) {
return &system.ResourceSnapshot{
CollectedAt: now,
CPU: system.CPUMetrics{UsagePercent: 27.5, Load1: 1.2},
Memory: system.MemoryMetrics{UsedPercent: 42},
History: []system.ResourcePoint{{Timestamp: now, CPUPercent: 27.5}},
}, nil
},
ProcessesProvider: func() (*system.FullProcessSnapshot, error) {
return &system.FullProcessSnapshot{
Processes: []system.FullProcess{{PID: 42, Name: "redflag-agent", User: "root"}},
ProcessCount: 1,
ScannedAt: now,
}, nil
},
ProcessDetailProvider: func(pid int, caps system.ProcessCaps) (*system.FullProcess, error) {
if pid != 42 {
t.Fatalf("pid = %d, want 42", pid)
}
if caps.MaxSockets != 500 || caps.MaxOpenFiles != 2000 {
t.Fatalf("default process caps = %#v", caps)
}
return &system.FullProcess{PID: pid, Name: "redflag-agent", Path: "/usr/bin/redflag-agent"}, nil
},
SoftwareProvider: func() (*system.SoftwareSnapshot, error) {
return &system.SoftwareSnapshot{
Supported: true,
Packages: []system.InstalledPackage{{PackageType: "pacman", Identity: "redflag-agent", Name: "redflag-agent", Version: "0.3.0", InstallReason: "explicit", Origin: "repository"}},
Count: 1,
}, nil
},
PackageDetailProvider: func(packageType, identity string) (*system.PackageDetail, error) {
if packageType != "pacman" || identity != "redflag-agent" {
t.Fatalf("package detail request = %s/%s", packageType, identity)
}
return &system.PackageDetail{InstalledPackage: system.InstalledPackage{PackageType: packageType, Identity: identity, Name: identity, Version: "0.3.0"}, DependsOn: []string{"glibc"}}, nil
},
ConnectionsProvider: func() (*system.ConnectionSnapshot, error) {
return &system.ConnectionSnapshot{Connections: []system.Connection{{PID: 42, Process: "redflag-agent", Protocol: "TCP", LocalPort: 443, State: "LISTEN"}}, Count: 1, CollectedAt: now}, nil
},
ServicesProvider: func() (*system.ServiceSnapshot, error) {
return &system.ServiceSnapshot{Manager: "systemd", Services: []system.Service{{Name: "redflag-agent", ActiveState: "active", SubState: "running"}}, Count: 1, Running: 1, CollectedAt: now}, nil
},
})
var monitor system.ResourceSnapshot
requestJSON(t, handler, http.MethodGet, "/v1/monitor", &monitor)
if monitor.CPU.UsagePercent != 27.5 || len(monitor.History) != 1 {
t.Fatalf("monitor = %#v", monitor)
}
var processes system.FullProcessSnapshot
requestJSON(t, handler, http.MethodGet, "/v1/processes", &processes)
if processes.ProcessCount != 1 || processes.Processes[0].Name != "redflag-agent" {
t.Fatalf("processes = %#v", processes)
}
var process system.FullProcess
requestJSON(t, handler, http.MethodGet, "/v1/processes/42", &process)
if process.Path != "/usr/bin/redflag-agent" {
t.Fatalf("process detail = %#v", process)
}
var software system.SoftwareSnapshot
requestJSON(t, handler, http.MethodGet, "/v1/software", &software)
if software.Count != 1 || software.Packages[0].Version != "0.3.0" {
t.Fatalf("software = %#v", software)
}
var packageDetail system.PackageDetail
requestJSON(t, handler, http.MethodGet, "/v1/software/detail?manager=pacman&identity=redflag-agent", &packageDetail)
if packageDetail.Name != "redflag-agent" || len(packageDetail.DependsOn) != 1 {
t.Fatalf("package detail = %#v", packageDetail)
}
var connections system.ConnectionSnapshot
requestJSON(t, handler, http.MethodGet, "/v1/connections", &connections)
if connections.Count != 1 || connections.Connections[0].State != "LISTEN" {
t.Fatalf("connections = %#v", connections)
}
var services system.ServiceSnapshot
requestJSON(t, handler, http.MethodGet, "/v1/services", &services)
if services.Running != 1 || services.Services[0].Name != "redflag-agent" {
t.Fatalf("services = %#v", services)
}
}
func TestProcessDetailRejectsInvalidPID(t *testing.T) {
handler := newHandler(Options{Config: testConfig(t), LoadCache: func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, nil
}})
req := httptest.NewRequest(http.MethodGet, "/v1/processes/not-a-pid", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
}
func TestCacheLoadFailureReturnsServiceUnavailable(t *testing.T) {
handler := newHandler(Options{Config: testConfig(t), LoadCache: func() (*cache.LocalCache, error) {
return nil, errors.New("cannot read cache")
}})
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(Options{Config: testConfig(t), LoadCache: func() (*cache.LocalCache, error) {
return &cache.LocalCache{}, 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",
}
}