Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
1071 lines
34 KiB
Go
1071 lines
34 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"crypto-miner-server/internal/builder"
|
|
"crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/models"
|
|
"crypto-miner-server/internal/pool"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type mockConfigProvider struct {
|
|
raw json.RawMessage
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetConfigJSON() json.RawMessage {
|
|
if len(m.raw) == 0 {
|
|
return json.RawMessage(`{"port":8989}`)
|
|
}
|
|
return m.raw
|
|
}
|
|
|
|
func (m *mockConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error {
|
|
m.raw = data
|
|
return nil
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetFleetAIConfig() FleetAIConfigView {
|
|
return FleetAIConfigView{
|
|
AIEndpoint: "http://127.0.0.1:11434/v1",
|
|
AINoContext: true,
|
|
AIDecisionIntervalSec: 60,
|
|
}
|
|
}
|
|
|
|
func (m *mockConfigProvider) UpdateFleetAIConfig(v FleetAIConfigView) error {
|
|
return nil
|
|
}
|
|
|
|
const testAuthUser = "testuser"
|
|
const testAuthPass = "testpass"
|
|
const testFleetSecret = "test-fleet-secret-integration"
|
|
|
|
func seedTestUsers(t *testing.T, dataDir string) {
|
|
t.Helper()
|
|
usersPath := filepath.Join(dataDir, "users.json")
|
|
data, err := json.Marshal(map[string]string{testAuthUser: testAuthPass})
|
|
if err != nil {
|
|
t.Fatalf("marshal users: %v", err)
|
|
}
|
|
if err := os.WriteFile(usersPath, data, 0600); err != nil {
|
|
t.Fatalf("write users.json: %v", err)
|
|
}
|
|
}
|
|
|
|
func newTestRouter(t *testing.T) (http.Handler, *WSHub, *db.Database, string) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
seedTestUsers(t, dataDir)
|
|
|
|
database, err := db.New(dataDir)
|
|
if err != nil {
|
|
t.Fatalf("db: %v", err)
|
|
}
|
|
t.Cleanup(func() { database.Close() })
|
|
|
|
wsHub := NewWSHub(database)
|
|
cfg := &mockConfigProvider{}
|
|
configHandler := NewConfigHandler(cfg)
|
|
aiHandler := NewAIHandler(database)
|
|
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{}, dataDir)
|
|
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
|
pathForgeHandler := builder.NewPathForgeHandler(dataDir)
|
|
blueprintHandler := NewBlueprintHandler(dataDir)
|
|
|
|
webRoot := filepath.Join(dataDir, "webroot")
|
|
_ = os.MkdirAll(webRoot, 0755)
|
|
_ = os.WriteFile(filepath.Join(webRoot, "index.html"), []byte("<html><body>AetherForge</body></html>"), 0644)
|
|
|
|
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
|
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
|
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
|
}
|
|
|
|
func serveAuthed(t *testing.T, router http.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
var req *http.Request
|
|
if body != nil {
|
|
req = httptest.NewRequest(method, path, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(method, path, nil)
|
|
}
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func serveAuthedMultipart(t *testing.T, router http.Handler, path string, body *bytes.Buffer, contentType string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body.Bytes()))
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func integrationWorkspaceRoot(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 10; i++ {
|
|
if _, err := os.Stat(filepath.Join(dir, "agent", "go.mod")); err == nil {
|
|
if _, err2 := os.Stat(filepath.Join(dir, "fusion", "main.go")); err2 == nil {
|
|
return dir
|
|
}
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
t.Skip("workspace root (agent/ and fusion/) not found")
|
|
return ""
|
|
}
|
|
|
|
func newFusionTestRouter(t *testing.T, projectRoot string) (http.Handler, *WSHub, *db.Database, string) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
seedTestUsers(t, dataDir)
|
|
|
|
database, err := db.New(dataDir)
|
|
if err != nil {
|
|
t.Fatalf("db: %v", err)
|
|
}
|
|
t.Cleanup(func() { database.Close() })
|
|
|
|
wsHub := NewWSHub(database)
|
|
cfg := &mockConfigProvider{}
|
|
configHandler := NewConfigHandler(cfg)
|
|
aiHandler := NewAIHandler(database)
|
|
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{}, dataDir)
|
|
builderHandler := builder.NewHandler(database, dataDir, filepath.Join(projectRoot, "agent"), projectRoot)
|
|
installFakeGoSuccess(t, builderHandler)
|
|
pathForgeHandler := builder.NewPathForgeHandler(dataDir)
|
|
blueprintHandler := NewBlueprintHandler(dataDir)
|
|
|
|
webRoot := filepath.Join(dataDir, "webroot")
|
|
_ = os.MkdirAll(webRoot, 0755)
|
|
_ = os.WriteFile(filepath.Join(webRoot, "index.html"), []byte("<html><body>AetherForge</body></html>"), 0644)
|
|
|
|
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
|
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
|
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
|
}
|
|
|
|
func fusionMultipartBody(t *testing.T) (*bytes.Buffer, string) {
|
|
t.Helper()
|
|
body := &bytes.Buffer{}
|
|
mw := multipart.NewWriter(body)
|
|
cfg, err := json.Marshal(builder.BuildRequest{
|
|
WorkerName: "integration-fusion",
|
|
ServerURL: "http://127.0.0.1:8989",
|
|
Wallet: "48abc",
|
|
TargetOS: "windows",
|
|
FusionEnabled: true,
|
|
FusionMediaMode: "paired",
|
|
FusionPayloadKind: "file",
|
|
FusionMediaBaseName: "report.pdf",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mw.WriteField("config", string(cfg)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
part, err := mw.CreateFormFile("prep_exe", "report.pdf")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := part.Write([]byte("%PDF-1.4 integration")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contentType := mw.FormDataContentType()
|
|
if err := mw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return body, contentType
|
|
}
|
|
|
|
func installFakeGoSuccess(t *testing.T, h *builder.Handler) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if runtime.GOOS == "windows" {
|
|
p := filepath.Join(dir, "go-ok.bat")
|
|
script := "@echo off\r\nsetlocal EnableDelayedExpansion\r\nset \"OUT=\"\r\n" +
|
|
":loop\r\nif \"%~1\"==\"\" goto done\r\nif /I \"%~1\"==\"-o\" (\r\n" +
|
|
" set \"OUT=%~2\"\r\n shift\r\n shift\r\n goto loop\r\n)\r\n" +
|
|
"shift\r\ngoto loop\r\n:done\r\n" +
|
|
"if defined OUT (\r\n" +
|
|
" for %%I in (\"!OUT!\") do if not exist \"%%~dpI\" mkdir \"%%~dpI\" 2>nul\r\n" +
|
|
" echo fake>\"!OUT!\"\r\n" +
|
|
")\r\nexit /b 0\r\n"
|
|
if err := os.WriteFile(p, []byte(script), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.SetGoBinPath(p)
|
|
return
|
|
}
|
|
p := filepath.Join(dir, "go-ok.sh")
|
|
script := "#!/bin/sh\nOUT=\"\"\nwhile [ $# -gt 0 ]; do\n" +
|
|
" if [ \"$1\" = \"-o\" ]; then OUT=\"$2\"; shift; fi\n shift\n" +
|
|
"done\nif [ -n \"$OUT\" ]; then mkdir -p \"$(dirname \"$OUT\")\"; echo fake > \"$OUT\"; fi\nexit 0\n"
|
|
if err := os.WriteFile(p, []byte(script), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.SetGoBinPath(p)
|
|
}
|
|
|
|
// serveWithFleetSecret sends a request with the fleet secret header (for /api/v1/agent/* routes).
|
|
func serveWithFleetSecret(t *testing.T, router http.Handler, method, path, secret string, body []byte) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
var req *http.Request
|
|
if body != nil {
|
|
req = httptest.NewRequest(method, path, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(method, path, nil)
|
|
}
|
|
req.Header.Set("X-Fleet-Secret", secret)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func insertTestBuild(t *testing.T, database *db.Database, dataDir, buildID, platform, fileName string) {
|
|
t.Helper()
|
|
buildDir := filepath.Join(dataDir, "builds", buildID)
|
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
binPath := filepath.Join(buildDir, fileName)
|
|
if err := os.WriteFile(binPath, []byte("fake-binary-"+platform), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.InsertBuild(&models.BuildRecord{
|
|
ID: buildID, WorkerName: "worker", ServerURL: "http://localhost:8989", Wallet: "48x",
|
|
FilePath: binPath, FileName: fileName, Platform: platform, CreatedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func startRouterServer(t *testing.T, router http.Handler) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(router)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func connectAgentViaRouter(t *testing.T, router http.Handler, agentID string) (*websocket.Conn, *httptest.Server) {
|
|
t.Helper()
|
|
srv := startRouterServer(t, router)
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/agent"
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("dial agent ws: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
|
|
authPayload, _ := json.Marshal(map[string]interface{}{
|
|
"agent_id": agentID,
|
|
"hostname": "integration-host",
|
|
"version": "1.0",
|
|
})
|
|
if err := conn.WriteJSON(Message{Type: "auth", Payload: authPayload}); err != nil {
|
|
t.Fatalf("send auth: %v", err)
|
|
}
|
|
var resp Message
|
|
if err := conn.ReadJSON(&resp); err != nil {
|
|
t.Fatalf("read auth_response: %v", err)
|
|
}
|
|
if resp.Type != "auth_response" {
|
|
t.Fatalf("expected auth_response, got %q", resp.Type)
|
|
}
|
|
return conn, srv
|
|
}
|
|
|
|
func TestHealthIsPublic(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("health status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]string
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["status"] != "ok" {
|
|
t.Fatalf("unexpected health: %v", body)
|
|
}
|
|
}
|
|
|
|
func TestConfigRequiresAuth(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestConfigWithValidAuth(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAgentsListRequiresAuth(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAgentsListAuthedEmpty(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
var agents []json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &agents); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(agents) != 0 {
|
|
t.Fatalf("expected empty fleet, got %d", len(agents))
|
|
}
|
|
}
|
|
|
|
func TestArtifactDownloadRejectsTraversal(t *testing.T) {
|
|
router, _, _, dataDir := newTestRouter(t)
|
|
buildID := "test-build-id"
|
|
buildDir := filepath.Join(dataDir, "builds", buildID)
|
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/..%2F..%2Fsecret.txt", nil)
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest && rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected rejection, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSPAServesIndex(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
if !contains(rec.Body.String(), "AetherForge") {
|
|
t.Fatalf("expected SPA fallback html")
|
|
}
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func TestStatsLimitCapped(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/nope/stats?limit=999999", nil)
|
|
req.SetBasicAuth(testAuthUser, testAuthPass)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
// Agent may not exist — 404 is fine; we only care handler doesn't 500 on huge limit
|
|
if rec.Code == http.StatusInternalServerError {
|
|
t.Fatalf("limit cap caused server error: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationServerInfo(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/server/info", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := body["port"]; !ok {
|
|
t.Fatalf("expected port in server info: %v", body)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationDashboardStats(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/dashboard/stats", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationGetAgentNotFound(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/agents/missing-agent", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAgentLog(t *testing.T) {
|
|
router, wsHub, _, _ := newTestRouter(t)
|
|
agentID := "log-agent"
|
|
connectTestAgent(t, wsHub, agentID)
|
|
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/agents/"+agentID+"/log", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["agent_id"] != agentID {
|
|
t.Fatalf("unexpected agent_id: %v", body["agent_id"])
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAgentCommandOffline(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/offline-agent/command",
|
|
[]byte(`{"action":"pause"}`))
|
|
// Command for a non-connected agent returns 200 with success:false (not a 4xx),
|
|
// so the caller can inspect the error without tripping HTTP error handling.
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["success"] != false {
|
|
t.Fatalf("expected success=false, got %v", body)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAgentMeta(t *testing.T) {
|
|
router, _, database, _ := newTestRouter(t)
|
|
agent := &models.Agent{ID: "meta-rig", Name: "rig", Status: "offline", LastSeen: time.Now()}
|
|
if err := database.UpsertAgent(agent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := serveAuthed(t, router, http.MethodPut, "/api/v1/agents/meta-rig/meta",
|
|
[]byte(`{"notes":"integration test","tags":["lab"]}`))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBulkCommandPartialFailure(t *testing.T) {
|
|
router, wsHub, _, _ := newTestRouter(t)
|
|
onlineID := "bulk-online"
|
|
connectTestAgent(t, wsHub, onlineID)
|
|
|
|
payload := `{"agent_ids":["` + onlineID + `","offline-one"],"action":"pause","command":"tasklist"}`
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/bulk-command", []byte(payload))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["success"] != true {
|
|
t.Fatalf("expected partial success true, got %v", body)
|
|
}
|
|
if body["sent"].(float64) != 1 || body["failed"].(float64) != 1 {
|
|
t.Fatalf("sent/failed counts: %v", body)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationFleetReadEndpoints(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
paths := []string{
|
|
"/api/v1/alerts",
|
|
"/api/v1/pools/status",
|
|
"/api/v1/ai/activity",
|
|
"/api/v1/earnings/estimate",
|
|
"/api/v1/shares",
|
|
"/api/v1/builds",
|
|
}
|
|
for _, path := range paths {
|
|
rec := serveAuthed(t, router, http.MethodGet, path, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("%s status=%d body=%s", path, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationMarketXMR(t *testing.T) {
|
|
setMockHTTPTransport(t, roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
if !strings.Contains(req.URL.String(), "coingecko") {
|
|
return nil, errors.New("unexpected url")
|
|
}
|
|
body := `{"monero":{"usd":165.5}}`
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
}, nil
|
|
}))
|
|
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/market/xmr", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["usd"].(float64) != 165.5 {
|
|
t.Fatalf("unexpected price: %v", body["usd"])
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBuildsLifecycle(t *testing.T) {
|
|
router, _, database, dataDir := newTestRouter(t)
|
|
buildID := "lifecycle-build"
|
|
insertTestBuild(t, database, dataDir, buildID, "windows", "worker.exe")
|
|
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/builds", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list status=%d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodPut, "/api/v1/builds/"+buildID+"/pin", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("pin status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builds/pin", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unpin status=%d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builds/"+buildID, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationPutConfig(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
rec := serveAuthed(t, router, http.MethodPut, "/api/v1/config", []byte(`{"port":9090}`))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "9090") {
|
|
t.Fatalf("expected updated config: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFusionMultipartEndToEndViaRouter(t *testing.T) {
|
|
projectRoot := integrationWorkspaceRoot(t)
|
|
router, _, database, _ := newFusionTestRouter(t, projectRoot)
|
|
|
|
body, contentType := fusionMultipartBody(t)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/build", bytes.NewReader(body.Bytes()))
|
|
req.Header.Set("Content-Type", contentType)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthed multipart forge expected 401, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
body, contentType = fusionMultipartBody(t)
|
|
rec = serveAuthedMultipart(t, router, "/api/v1/builder/build", body, contentType)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("authed fusion multipart status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp builder.BuildResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode build response: %v body=%s", err, rec.Body.String())
|
|
}
|
|
if !resp.Success {
|
|
t.Fatalf("expected success=true, got %+v", resp)
|
|
}
|
|
if resp.BuildID == "" {
|
|
t.Fatal("expected build_id in response")
|
|
}
|
|
|
|
record, err := database.GetBuild(resp.BuildID)
|
|
if err != nil {
|
|
t.Fatalf("build not in database: %v", err)
|
|
}
|
|
if record.WorkerName != "integration-fusion" {
|
|
t.Fatalf("worker_name: got %q want integration-fusion", record.WorkerName)
|
|
}
|
|
if record.Platform != "windows" {
|
|
t.Fatalf("platform: got %q want windows", record.Platform)
|
|
}
|
|
|
|
builds, err := database.ListBuilds(10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(builds) != 1 {
|
|
t.Fatalf("expected 1 build in DB, got %d", len(builds))
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBuilderRoutes(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/builder/build", []byte(`{}`))
|
|
if rec.Code == http.StatusNotFound {
|
|
t.Fatal("builder/build route not registered")
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodPost, "/api/v1/builder/estimate", []byte("not-multipart"))
|
|
if rec.Code == http.StatusNotFound {
|
|
t.Fatal("builder/estimate route not registered")
|
|
}
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("estimate without multipart expected 400, got %d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builder/cancel/no-such-token", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("cancel missing token expected 404, got %d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodPost, "/api/v1/builder/path-forge", []byte(`{}`))
|
|
if rec.Code == http.StatusNotFound {
|
|
t.Fatal("builder/path-forge route not registered")
|
|
}
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("path-forge without root_path expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBlueprintsCRUD(t *testing.T) {
|
|
router, _, _, dataDir := newTestRouter(t)
|
|
|
|
saveBody, _ := json.Marshal(map[string]interface{}{
|
|
"name": "integration-preset",
|
|
"data": map[string]interface{}{"threads": 2},
|
|
})
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/blueprints", saveBody)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("save status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
filePath := filepath.Join(dataDir, "blueprints", "integration-preset.json")
|
|
if _, err := os.Stat(filePath); err != nil {
|
|
t.Fatalf("blueprint file missing: %v", err)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list status=%d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints/integration-preset", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("get status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/blueprints?name=integration-preset", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBlueprintErrors(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/blueprints", []byte(`{"name":"","data":{}}`))
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("empty name expected 400, got %d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints/missing-preset", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("missing blueprint expected 404, got %d", rec.Code)
|
|
}
|
|
|
|
rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/blueprints", nil)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("delete without name expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAIRoutes(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
|
|
// Agent routes require fleet secret (not Basic Auth).
|
|
SetAgentPathSecret(testFleetSecret)
|
|
t.Cleanup(func() { SetAgentPathSecret("") })
|
|
|
|
// decide: missing agent_id → 400.
|
|
rec := serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/decide", testFleetSecret, []byte(`{"worker_name":"x"}`))
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("decide missing agent_id expected 400, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// report and heartbeat require fleet secret; they don't need a pre-registered engine.
|
|
body, _ := json.Marshal(map[string]string{"agent_id": "report-agent", "tool": "sleep", "output": "ok"})
|
|
rec = serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/report", testFleetSecret, body)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("report status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
hb, _ := json.Marshal(map[string]string{"agent_id": "hb-agent", "status": "alive"})
|
|
rec = serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/heartbeat", testFleetSecret, hb)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("heartbeat status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAgentRoutesFleetSecret(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
SetAgentPathSecret("integration-fleet-secret")
|
|
t.Cleanup(func() { SetAgentPathSecret("") })
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/heartbeat",
|
|
bytes.NewReader([]byte(`{"agent_id":"a","status":"alive"}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("missing fleet secret expected 403, got %d", rec.Code)
|
|
}
|
|
|
|
req.Header.Set("X-Fleet-Secret", "integration-fleet-secret")
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("valid fleet secret expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIntegrationDropperVariants(t *testing.T) {
|
|
router, _, database, dataDir := newTestRouter(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/get", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("no builds expected 404, got %d", rec.Code)
|
|
}
|
|
|
|
insertTestBuild(t, database, dataDir, "win-drop", "windows", "worker.exe")
|
|
req = httptest.NewRequest(http.MethodGet, "/get?os=windows", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("windows build expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/get?os=linux", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("linux with only windows build falls back to latest, expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/get", nil)
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0)")
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("UA-detected windows expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
for _, path := range []string{"/install.sh", "/install.ps1"} {
|
|
req = httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Host = "forge.local:8989"
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("%s status=%d", path, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationBuildUninstallNotFound(t *testing.T) {
|
|
router, _, database, dataDir := newTestRouter(t)
|
|
buildID := "uninstall-build"
|
|
insertTestBuild(t, database, dataDir, buildID, "windows", "worker.exe")
|
|
|
|
rec := serveAuthed(t, router, http.MethodGet, "/api/v1/builds/"+buildID+"/uninstall", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("missing uninstall script expected 404, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationRouterWebSocketDashboard(t *testing.T) {
|
|
router, _, _, _ := newTestRouter(t)
|
|
srv := startRouterServer(t, router)
|
|
|
|
badURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/dashboard"
|
|
_, resp, err := websocket.DefaultDialer.Dial(badURL, nil)
|
|
if err == nil {
|
|
t.Fatal("expected dial failure without token")
|
|
}
|
|
if resp == nil || resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without token, got err=%v status=%v", err, resp)
|
|
}
|
|
|
|
goodURL := badURL + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
|
|
conn, resp, err := websocket.DefaultDialer.Dial(goodURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("dial with token: %v status=%v", err, resp)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
|
|
var msg Message
|
|
if err := conn.ReadJSON(&msg); err != nil {
|
|
t.Fatalf("read init: %v", err)
|
|
}
|
|
if msg.Type != "init" {
|
|
t.Fatalf("expected init, got %q", msg.Type)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationRouterWebSocketAgentBadSecret(t *testing.T) {
|
|
router, wsHub, _, _ := newTestRouter(t)
|
|
wsHub.SetFleetSecret("ws-fleet-secret")
|
|
t.Cleanup(func() { wsHub.SetFleetSecret("") })
|
|
|
|
srv := startRouterServer(t, router)
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/agent"
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("dial agent ws: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
|
|
resp := authAgentConn(t, conn, map[string]interface{}{
|
|
"agent_id": "bad-secret-agent", "fleet_secret": "wrong", "hostname": "host",
|
|
})
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(resp.Payload, &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["success"] != false {
|
|
t.Fatalf("expected auth failure with bad fleet secret, got %+v", body)
|
|
}
|
|
if wsHub.isAgentConnected("bad-secret-agent") {
|
|
t.Fatal("agent should not register with bad fleet secret")
|
|
}
|
|
}
|
|
|
|
func TestIntegrationRouterWebSocketAgentConnectedCommand(t *testing.T) {
|
|
router, wsHub, _, _ := newTestRouter(t)
|
|
agentID := "router-cmd-agent"
|
|
connectAgentViaRouter(t, router, agentID)
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if wsHub.isAgentConnected(agentID) {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if !wsHub.isAgentConnected(agentID) {
|
|
t.Fatal("agent not connected via router ws")
|
|
}
|
|
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/"+agentID+"/command",
|
|
[]byte(`{"action":"pause"}`))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("command status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestIntegrationRouterCommandFullRoundTrip validates the full remote-command path
|
|
// through the HTTP router: POST /api/v1/agents/{id}/command → agent WS receives
|
|
// command → simulated agent sends command_result → dashboard WS receives broadcast.
|
|
func TestIntegrationRouterCommandFullRoundTrip(t *testing.T) {
|
|
router, wsHub, _, _ := newTestRouter(t)
|
|
agentID := "router-roundtrip-agent"
|
|
const testAction = "exec"
|
|
const testCommand = "whoami"
|
|
const resultMessage = "integration round-trip ok"
|
|
|
|
agentConn, srv := connectAgentViaRouter(t, router, agentID)
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if wsHub.isAgentConnected(agentID) {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if !wsHub.isAgentConnected(agentID) {
|
|
t.Fatal("agent not connected via router ws")
|
|
}
|
|
|
|
dashURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/dashboard?token=" + wsDashboardToken(testAuthUser, testAuthPass)
|
|
dashConn, _, err := websocket.DefaultDialer.Dial(dashURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("dial dashboard ws: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dashConn.Close() })
|
|
|
|
type msgResult struct {
|
|
body map[string]interface{}
|
|
err string
|
|
}
|
|
cmdResultCh := make(chan msgResult, 1)
|
|
go func() {
|
|
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
for {
|
|
var msg Message
|
|
if err := dashConn.ReadJSON(&msg); err != nil {
|
|
cmdResultCh <- msgResult{err: err.Error()}
|
|
return
|
|
}
|
|
if msg.Type != "command_result" {
|
|
continue
|
|
}
|
|
var body map[string]interface{}
|
|
if parseErr := json.Unmarshal(msg.Payload, &body); parseErr != nil {
|
|
cmdResultCh <- msgResult{err: "parse: " + parseErr.Error()}
|
|
return
|
|
}
|
|
cmdResultCh <- msgResult{body: body}
|
|
return
|
|
}
|
|
}()
|
|
|
|
type agentCmdResult struct {
|
|
cmd Message
|
|
err string
|
|
}
|
|
agentCmdCh := make(chan agentCmdResult, 1)
|
|
go func() {
|
|
_ = agentConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
var cmd Message
|
|
if err := agentConn.ReadJSON(&cmd); err != nil {
|
|
agentCmdCh <- agentCmdResult{err: err.Error()}
|
|
return
|
|
}
|
|
agentCmdCh <- agentCmdResult{cmd: cmd}
|
|
|
|
cmdPayload, _ := json.Marshal(map[string]interface{}{
|
|
"action": testAction,
|
|
"success": true,
|
|
"message": resultMessage,
|
|
})
|
|
if err := agentConn.WriteJSON(Message{Type: "command_result", Payload: cmdPayload}); err != nil {
|
|
agentCmdCh <- agentCmdResult{err: "send command_result: " + err.Error()}
|
|
}
|
|
}()
|
|
|
|
cmdBody, _ := json.Marshal(map[string]string{
|
|
"action": testAction,
|
|
"command": testCommand,
|
|
})
|
|
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/"+agentID+"/command", cmdBody)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("command status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var httpBody map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &httpBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if httpBody["success"] != true {
|
|
t.Fatalf("expected success=true, got %v", httpBody)
|
|
}
|
|
if httpBody["action"] != testAction {
|
|
t.Fatalf("http action: got %v, want %s", httpBody["action"], testAction)
|
|
}
|
|
|
|
select {
|
|
case r := <-agentCmdCh:
|
|
if r.err != "" {
|
|
t.Fatalf("agent did not receive command: %s", r.err)
|
|
}
|
|
if r.cmd.Type != "command" {
|
|
t.Fatalf("agent expected command, got %q", r.cmd.Type)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(r.cmd.Payload, &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["action"] != testAction {
|
|
t.Errorf("agent command action: got %v, want %s", payload["action"], testAction)
|
|
}
|
|
if payload["command"] != testCommand {
|
|
t.Errorf("agent command: got %v, want %s", payload["command"], testCommand)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for agent command")
|
|
}
|
|
|
|
select {
|
|
case r := <-cmdResultCh:
|
|
if r.err != "" {
|
|
t.Fatalf("dashboard did not receive command_result: %s", r.err)
|
|
}
|
|
if r.body["agent_id"] != agentID {
|
|
t.Errorf("dashboard agent_id: got %v, want %s", r.body["agent_id"], agentID)
|
|
}
|
|
if r.body["action"] != testAction {
|
|
t.Errorf("dashboard action: got %v, want %s", r.body["action"], testAction)
|
|
}
|
|
if r.body["message"] != resultMessage {
|
|
t.Errorf("dashboard message: got %v, want %q", r.body["message"], resultMessage)
|
|
}
|
|
if r.body["success"] != true {
|
|
t.Errorf("dashboard success: got %v, want true", r.body["success"])
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for command_result broadcast")
|
|
}
|
|
}
|