mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 10:40:13 +00:00
test(agentcompat): add dashboard runtime harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
//go:build linux
|
||||
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
|
||||
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/testpaths"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/workspace"
|
||||
)
|
||||
|
||||
type failingDashboardSupervisor struct{}
|
||||
|
||||
func (failingDashboardSupervisor) Start() error { return nil }
|
||||
|
||||
func (failingDashboardSupervisor) Stop(context.Context) error {
|
||||
return errors.New("injected supervisor cleanup failure")
|
||||
}
|
||||
|
||||
func (failingDashboardSupervisor) Exited() <-chan struct{} {
|
||||
return make(chan struct{})
|
||||
}
|
||||
|
||||
func (failingDashboardSupervisor) PID() int { return 0 }
|
||||
|
||||
func (failingDashboardSupervisor) ProcessGroupID() int { return 0 }
|
||||
|
||||
func (failingDashboardSupervisor) CleanupRecord() processharness.CleanupRecord {
|
||||
return processharness.CleanupRecord{Name: "dashboard", Error: "injected supervisor cleanup failure"}
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_RecreatesFreshStateAfterShutdown(t *testing.T) {
|
||||
// Given
|
||||
first := startDashboardWithoutCleanup(t, false)
|
||||
firstDatabasePath := first.DatabasePath()
|
||||
firstWorkspaceRoot := first.WorkspaceRoot()
|
||||
stopContext, cancel := context.WithTimeout(t.Context(), 15*time.Second)
|
||||
require.NoError(t, first.Stop(stopContext))
|
||||
cancel()
|
||||
|
||||
// When
|
||||
second := startDashboard(t, false)
|
||||
|
||||
// Then
|
||||
require.NotEqual(t, firstDatabasePath, second.DatabasePath())
|
||||
require.NotEqual(t, firstWorkspaceRoot, second.WorkspaceRoot())
|
||||
require.NoDirExists(t, firstWorkspaceRoot)
|
||||
require.True(t, second.Bootstrap().LoginAuthenticated)
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_ContextInterruptionCleansProcessAndWorkspace(t *testing.T) {
|
||||
// Given
|
||||
processContext, interrupt := context.WithCancel(t.Context())
|
||||
sourceDir, err := testpaths.NezhaSource(t.Name())
|
||||
require.NoError(t, err)
|
||||
dashboard, err := Start(processContext, StartConfig{SourceDir: sourceDir})
|
||||
require.NoError(t, err)
|
||||
root := dashboard.WorkspaceRoot()
|
||||
pid := dashboard.PID()
|
||||
t.Cleanup(func() {
|
||||
cleanupContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, dashboard.Stop(cleanupContext))
|
||||
})
|
||||
|
||||
// When
|
||||
interrupt()
|
||||
requireDashboardCleanup(t, dashboard)
|
||||
|
||||
// Then
|
||||
require.NoDirExists(t, root)
|
||||
require.NoFileExists(t, filepath.Join("/proc", strconv.Itoa(pid)))
|
||||
require.True(t, dashboard.CleanupReceipt().Passed)
|
||||
require.False(t, dashboard.CleanupReceipt().Forced)
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_StopDeadlineStillCleansProcessAndWorkspace(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboardWithoutCleanup(t, false)
|
||||
root := dashboard.WorkspaceRoot()
|
||||
pid := dashboard.PID()
|
||||
stopContext, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
t.Cleanup(func() {
|
||||
cleanupContext, cleanupCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cleanupCancel()
|
||||
require.NoError(t, dashboard.Stop(cleanupContext))
|
||||
})
|
||||
|
||||
// When
|
||||
stopError := dashboard.Stop(stopContext)
|
||||
|
||||
// Then
|
||||
require.ErrorIs(t, stopError, context.Canceled)
|
||||
requireDashboardCleanup(t, dashboard)
|
||||
require.NoDirExists(t, root)
|
||||
require.NoFileExists(t, filepath.Join("/proc", strconv.Itoa(pid)))
|
||||
require.True(t, dashboard.CleanupReceipt().Passed)
|
||||
require.False(t, dashboard.CleanupReceipt().Forced)
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_SupervisorErrorStillClosesWorkspace(t *testing.T) {
|
||||
// Given
|
||||
workspaceRoot, err := workspace.New(t.Context())
|
||||
require.NoError(t, err)
|
||||
dashboard := &Dashboard{
|
||||
workspace: workspaceRoot,
|
||||
supervisor: failingDashboardSupervisor{},
|
||||
cleanupDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
// When
|
||||
stopContext, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
stopError := dashboard.Stop(stopContext)
|
||||
|
||||
// Then
|
||||
require.ErrorContains(t, stopError, "injected supervisor cleanup failure")
|
||||
require.NoDirExists(t, workspaceRoot.Root())
|
||||
require.False(t, dashboard.CleanupReceipt().Passed)
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_HungProcessRespectsReadinessDeadline(t *testing.T) {
|
||||
// Given
|
||||
workspaceParent := t.TempDir()
|
||||
t.Setenv("TMPDIR", workspaceParent)
|
||||
sourceDir := writeHungDashboardSource(t)
|
||||
requireNoWorkspaceEntries(t, workspaceParent)
|
||||
|
||||
// When
|
||||
_, err := Start(t.Context(), StartConfig{
|
||||
SourceDir: sourceDir,
|
||||
ReadinessTimeout: 200 * time.Millisecond,
|
||||
})
|
||||
|
||||
// Then
|
||||
require.ErrorContains(t, err, "dashboard login readiness")
|
||||
requireNoWorkspaceEntries(t, workspaceParent)
|
||||
}
|
||||
|
||||
func TestDashboardAdversarial_RejectsMisleadingHTTP200Login(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboard(t, false)
|
||||
requestBody := []byte(`{"username":"admin","password":"wrong-password"}`)
|
||||
request, err := http.NewRequestWithContext(t.Context(), http.MethodPost, dashboard.URL()+"/api/v1/login", bytes.NewReader(requestBody))
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// When
|
||||
response, err := dashboard.restHTTPClient.Do(request)
|
||||
require.NoError(t, err)
|
||||
defer response.Body.Close()
|
||||
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 4096))
|
||||
require.NoError(t, err)
|
||||
var envelope client.CommonResponse[json.RawMessage]
|
||||
require.NoError(t, json.Unmarshal(responseBody, &envelope))
|
||||
|
||||
// Then
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.False(t, envelope.Success)
|
||||
require.Contains(t, envelope.Error, "Unauthorized")
|
||||
}
|
||||
|
||||
func writeHungDashboardSource(t *testing.T) string {
|
||||
t.Helper()
|
||||
sourceDir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(sourceDir, "cmd", "dashboard"), 0o700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(sourceDir, "go.mod"), []byte("module example.com/hungdashboard\n\ngo 1.23\n"), 0o600))
|
||||
program := `package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
<-signals
|
||||
}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(sourceDir, "cmd", "dashboard", "main.go"), []byte(program), 0o600))
|
||||
return sourceDir
|
||||
}
|
||||
|
||||
func requireNoWorkspaceEntries(t *testing.T, workspaceParent string) {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(workspaceParent)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, entries)
|
||||
}
|
||||
|
||||
func requireDashboardCleanup(t *testing.T, dashboard *Dashboard) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-dashboard.cleanupDone:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("dashboard cleanup did not complete")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user