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("AetherForge"), 0644) dropperHandler := NewDropperHandler(database, nil) return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, dropperHandler, 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()) } }