feat: add auto update

This commit is contained in:
2026-05-19 15:53:30 +02:00
parent bd3121d688
commit ba4de62a34
22 changed files with 3323 additions and 110 deletions

View File

@ -2,6 +2,7 @@ package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@ -801,3 +802,246 @@ func TestContainerAction_Success(t *testing.T) {
t.Error("expected command_id in response")
}
}
// newCancelledRequest creates a request with an already-cancelled context.
func newCancelledRequest(method, target string, body *bytes.Reader) *http.Request {
ctx, cancel := context.WithCancel(context.Background())
cancel()
var req *http.Request
if body != nil {
req = httptest.NewRequest(method, target, body)
} else {
req = httptest.NewRequest(method, target, nil)
}
return req.WithContext(ctx)
}
// ── FsList ────────────────────────────────────────────────────────────────────
func TestFsList_AgentNotFound(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Get("/api/v1/agents/{agentID}/fs/list", h.FsList)
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/ghost/fs/list?path=/tmp", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestFsList_Timeout(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Get("/api/v1/agents/{agentID}/fs/list", h.FsList)
// Use cancelled context to force immediate timeout on the agent wait.
req := newCancelledRequest(http.MethodGet, "/api/v1/agents/a1/fs/list?path=/tmp", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Either 504 (timeout) or 404 (send failed because channel was full/cancelled).
if w.Code != http.StatusGatewayTimeout && w.Code != http.StatusNotFound {
t.Errorf("expected 504 or 404, got %d", w.Code)
}
}
func TestFsList_MissingPath(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Get("/api/v1/agents/{agentID}/fs/list", h.FsList)
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a1/fs/list", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
// ── FsRead ────────────────────────────────────────────────────────────────────
func TestFsRead_AgentNotFound(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Get("/api/v1/agents/{agentID}/fs/read", h.FsRead)
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/ghost/fs/read?path=/etc/hosts", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestFsRead_MissingPath(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Get("/api/v1/agents/{agentID}/fs/read", h.FsRead)
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a1/fs/read", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
// ── FsWrite ───────────────────────────────────────────────────────────────────
func TestFsWrite_AgentNotFound(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/fs/write", h.FsWrite)
body, _ := json.Marshal(map[string]string{"path": "/tmp/test.txt", "content": "hello"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/ghost/fs/write", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestFsWrite_MissingPath(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/fs/write", h.FsWrite)
body, _ := json.Marshal(map[string]string{"content": "hello"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a1/fs/write", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
// ── FsMkdir ───────────────────────────────────────────────────────────────────
func TestFsMkdir_AgentNotFound(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/fs/mkdir", h.FsMkdir)
body, _ := json.Marshal(map[string]string{"path": "/opt/stacks/nouveau-dossier"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/ghost/fs/mkdir", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestFsMkdir_InvalidBody(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/fs/mkdir", h.FsMkdir)
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a1/fs/mkdir", bytes.NewReader([]byte("not-json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
// ── ComposeAction ─────────────────────────────────────────────────────────────
func TestComposeAction_AgentNotFound(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/compose", h.ComposeAction)
body, _ := json.Marshal(map[string]string{"path": "/opt/stack", "action": "up"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/ghost/compose", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestComposeAction_InvalidAction(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/compose", h.ComposeAction)
body, _ := json.Marshal(map[string]string{"path": "/opt/stack", "action": "restart"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a1/compose", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestComposeAction_MissingFields(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/compose", h.ComposeAction)
body, _ := json.Marshal(map[string]string{"action": "up"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/ghost/compose", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestComposeAction_Timeout(t *testing.T) {
h, _, reg, _ := newTestHandler(t)
reg.Register("a1", "h", "a", "ip", "arch", "os")
router := chi.NewRouter()
router.Post("/api/v1/agents/{agentID}/compose", h.ComposeAction)
bodyBytes, _ := json.Marshal(map[string]string{"path": "/opt/stack", "action": "up"})
req := newCancelledRequest(http.MethodPost, "/api/v1/agents/a1/compose", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusGatewayTimeout && w.Code != http.StatusNotFound {
t.Errorf("expected 504 or 404, got %d", w.Code)
}
}