Add fleet registry removal with confirm modal and agent_removed WS.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators can remove machines from Crucible and dashboard rosters with honest messaging that deletion is registry-only; bulk select, oath ledger entries, and Go/Vitest coverage included.
This commit is contained in:
@@ -78,6 +78,8 @@ func fleetChiRoute(method, pattern string, handler http.HandlerFunc) http.Handle
|
||||
r.Post(pattern, handler)
|
||||
case http.MethodPut:
|
||||
r.Put(pattern, handler)
|
||||
case http.MethodDelete:
|
||||
r.Delete(pattern, handler)
|
||||
default:
|
||||
panic("unsupported method " + method)
|
||||
}
|
||||
@@ -989,3 +991,134 @@ func TestFleetPostAgentCommandEmptyAgentID(t *testing.T) {
|
||||
t.Fatalf("expected 400 for empty id, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func readDashboardWSMessage(t *testing.T, conn *websocket.Conn, wantType string) Message {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
var msg Message
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
t.Fatalf("read %s: %v", wantType, err)
|
||||
}
|
||||
if msg.Type == wantType {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
t.Fatalf("timed out waiting for %s", wantType)
|
||||
return Message{}
|
||||
}
|
||||
|
||||
func TestFleetDeleteAgentSuccess(t *testing.T) {
|
||||
fh, database, ws, _ := newTestFleetHandler(t)
|
||||
agent := &models.Agent{ID: "del-agent-1", Name: "rig", Status: "offline", LastSeen: time.Now()}
|
||||
if err := database.UpsertAgent(agent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dashConn := connectTestDashboard(t, ws)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/agents/del-agent-1", nil)
|
||||
fleetChiRoute(http.MethodDelete, "/agents/{id}", fh.DeleteAgent).ServeHTTP(rec, req)
|
||||
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("unexpected body: %v", body)
|
||||
}
|
||||
if _, err := database.GetAgent("del-agent-1"); err == nil {
|
||||
t.Fatal("expected agent removed from database")
|
||||
}
|
||||
|
||||
removed := readDashboardWSMessage(t, dashConn, "agent_removed")
|
||||
var payload map[string]string
|
||||
if err := json.Unmarshal(removed.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload["agent_id"] != "del-agent-1" {
|
||||
t.Fatalf("agent_id = %q", payload["agent_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetDeleteAgentEmptyID(t *testing.T) {
|
||||
fh, _, _, _ := newTestFleetHandler(t)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", "")
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
fh.DeleteAgent(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetDeleteAgentDisconnectsOnline(t *testing.T) {
|
||||
fh, database, ws, _ := newTestFleetHandler(t)
|
||||
agentID := "del-online"
|
||||
connectTestAgent(t, ws, agentID)
|
||||
if !ws.isAgentConnected(agentID) {
|
||||
t.Fatal("agent should be connected")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/agents/"+agentID, nil)
|
||||
fleetChiRoute(http.MethodDelete, "/agents/{id}", fh.DeleteAgent).ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if ws.isAgentConnected(agentID) {
|
||||
t.Fatal("agent should be disconnected after delete")
|
||||
}
|
||||
if _, err := database.GetAgent(agentID); err == nil {
|
||||
t.Fatal("expected agent removed from database")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetBulkDeleteAgents(t *testing.T) {
|
||||
fh, database, ws, _ := newTestFleetHandler(t)
|
||||
for _, id := range []string{"bulk-del-a", "bulk-del-b", "bulk-del-missing"} {
|
||||
if id == "bulk-del-missing" {
|
||||
continue
|
||||
}
|
||||
agent := &models.Agent{ID: id, Name: id, Status: "offline", LastSeen: time.Now()}
|
||||
if err := database.UpsertAgent(agent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
connectTestAgent(t, ws, "bulk-del-a")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/agents/bulk-delete",
|
||||
strings.NewReader(`{"ids":["bulk-del-a","bulk-del-b","bulk-del-missing"]}`))
|
||||
fh.BulkDeleteAgents(rec, req)
|
||||
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["deleted"].(float64) != 2 {
|
||||
t.Fatalf("deleted count = %v", body["deleted"])
|
||||
}
|
||||
if _, err := database.GetAgent("bulk-del-a"); err == nil {
|
||||
t.Fatal("bulk-del-a should be gone")
|
||||
}
|
||||
if _, err := database.GetAgent("bulk-del-b"); err == nil {
|
||||
t.Fatal("bulk-del-b should be gone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetBulkDeleteAgentsRequiresIDs(t *testing.T) {
|
||||
fh, _, _, _ := newTestFleetHandler(t)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/agents/bulk-delete", strings.NewReader(`{"ids":[]}`))
|
||||
fh.BulkDeleteAgents(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user