Introduce test.bat orchestrating Go/Vitest/Playwright phases, expand coverage across server, agent, and dashboard, and document in tests/README.md.
174 lines
5.0 KiB
Go
174 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"crypto-miner-server/internal/builder"
|
|
"crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/pool"
|
|
)
|
|
|
|
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 newTestRouter(t *testing.T) (http.Handler, string) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
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(database, cfg)
|
|
aiHandler := NewAIHandler(database)
|
|
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{})
|
|
builderHandler := builder.NewHandler(database, dataDir, "", 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)
|
|
|
|
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, webRoot, dataDir, nil), dataDir
|
|
}
|
|
|
|
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("drjones", "czapiewski")
|
|
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("drjones", "czapiewski")
|
|
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)
|
|
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("drjones", "czapiewski")
|
|
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())
|
|
}
|
|
}
|