168 lines
4.9 KiB
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()
|
|
}
|