Files
AetherForge/server/internal/api/pathtracer_discover_test.go

168 lines
4.9 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crypto-miner-server/internal/db"
)
func TestMergeServiceGraph(t *testing.T) {
base := map[string]ServiceGraphHost{
"10.0.0.5": {
Host: "10.0.0.5",
Services: []ServiceGraphEntry{
{ServiceName: "smb", Port: 445, JoinLaneCandidate: "smb"},
},
},
}
delta := map[string]ServiceGraphHost{
"10.0.0.5": {
Host: "10.0.0.5",
Services: []ServiceGraphEntry{
{ServiceName: "winrm", Port: 5985, JoinLaneCandidate: "winrm"},
},
},
"10.0.0.9": {
Host: "10.0.0.9",
Services: []ServiceGraphEntry{
{ServiceName: "ssh", Port: 22, JoinLaneCandidate: "linux"},
},
},
}
merged := mergeServiceGraph(base, delta)
if len(merged) != 2 || len(merged["10.0.0.5"].Services) != 2 {
t.Fatalf("merged = %+v", merged)
}
}
func TestParseAgentDiscoverJSON(t *testing.T) {
raw := `log prefix
{"probed_at":"2026-06-06T12:00:00Z","local":{"host":"10.1.2.3","subnet":"10.1.2","services":[{"service_name":"docker","join_lane_candidate":"docker"}]},"lan_hosts":[{"host":"10.1.2.40","subnet":"10.1.2","services":[{"service_name":"smb","port":445,"join_lane_candidate":"smb","source":"lan_port"}]}]}`
payload, err := parseAgentDiscoverJSON(raw)
if err != nil {
t.Fatal(err)
}
if payload.Local.Host != "10.1.2.3" || len(payload.LANHosts) != 1 {
t.Fatalf("payload = %+v", payload)
}
}
func TestPathTracerDiscoverValidation(t *testing.T) {
h := NewPathTracerHandler(NewWSHub(nil))
req := httptest.NewRequest(http.MethodPost, "/pathtrace/discover", strings.NewReader(`{}`))
rec := httptest.NewRecorder()
h.Discover(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestPathTracerDiscoverMergesHopResults(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
agentID := "discover-hop-agent"
hub := NewWSHub(database)
handler := NewPathTracerHandler(hub)
conn := connectTestAgent(t, hub, agentID)
sess := testTraceSession(1)
sess.Hops[0].AgentID = agentID
handler.mu.Lock()
handler.sessions[sess.ID] = sess
handler.mu.Unlock()
fixture := `{"probed_at":"2026-06-06T12:00:00Z","local":{"host":"192.168.1.10","subnet":"192.168.1","services":[{"service_name":"CCMEXEC","join_lane_candidate":"gpo","source":"local_service"}]},"lan_hosts":[{"host":"192.168.1.50","subnet":"192.168.1","services":[{"service_name":"smb","port":445,"join_lane_candidate":"smb","source":"lan_port"}]}],"passive_hints":["domain_joined"]}`
go func() {
for {
var msg Message
if err := conn.ReadJSON(&msg); err != nil {
return
}
if msg.Type != "command" {
continue
}
var payload map[string]interface{}
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
continue
}
if payload["action"] == "service_discover" {
_ = conn.WriteJSON(Message{Type: "command_result", Payload: mustMarshal(map[string]interface{}{
"action": "service_discover", "success": true, "message": fixture,
})})
return
}
}
}()
body := fmt.Sprintf(`{"session_id":%q,"max_hosts":16}`, sess.ID)
req := httptest.NewRequest(http.MethodPost, "/pathtrace/discover", strings.NewReader(body))
rec := httptest.NewRecorder()
handler.Discover(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("discover status=%d body=%s", rec.Code, rec.Body.String())
}
var resp struct {
OK bool `json:"ok"`
ServiceGraph []ServiceGraphHost `json:"service_graph"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if !resp.OK || len(resp.ServiceGraph) < 2 {
var errBody map[string]interface{}
_ = json.Unmarshal(rec.Body.Bytes(), &errBody)
t.Fatalf("resp = %+v body=%v", resp, errBody)
}
handler.mu.Lock()
stored := handler.sessions[sess.ID]
handler.mu.Unlock()
if len(stored.ServiceGraph) < 2 || stored.DiscoveredAt == nil {
t.Fatalf("stored graph = %+v discovered_at=%v", stored.ServiceGraph, stored.DiscoveredAt)
}
if stored.DiscoverInProgress {
t.Fatal("discover should not remain in progress")
}
}
func TestPathTracerDiscoverConflictWhileInProgress(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
agentID := "discover-busy-agent"
hub := NewWSHub(database)
handler := NewPathTracerHandler(hub)
_ = connectTestAgent(t, hub, agentID)
sess := testTraceSession(1)
sess.Hops[0].AgentID = agentID
sess.DiscoverInProgress = true
handler.mu.Lock()
handler.sessions[sess.ID] = sess
handler.mu.Unlock()
body := fmt.Sprintf(`{"session_id":%q}`, sess.ID)
req := httptest.NewRequest(http.MethodPost, "/pathtrace/discover", strings.NewReader(body))
rec := httptest.NewRecorder()
handler.Discover(rec, req)
if rec.Code != http.StatusConflict {
t.Fatalf("expected 409, got %d body=%s", rec.Code, rec.Body.String())
}
_ = time.Now()
}