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,216 @@
|
||||
//go:build linux
|
||||
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/testpaths"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
)
|
||||
|
||||
func TestDashboard_BootstrapsSQLiteLoginPATAndMCP(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboard(t, false)
|
||||
bootstrap := dashboard.Bootstrap()
|
||||
|
||||
// When
|
||||
database, err := gorm.Open(sqlite.Open(dashboard.DatabasePath()), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
var userCount int64
|
||||
require.NoError(t, database.Model(&model.User{}).Count(&userCount).Error)
|
||||
var tokenCount int64
|
||||
require.NoError(t, database.Model(&model.APIToken{}).Count(&tokenCount).Error)
|
||||
require.True(t, database.Migrator().HasTable(&model.MCPAuditLog{}))
|
||||
sqlDatabase, err := database.DB()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqlDatabase.Close())
|
||||
configData, err := os.ReadFile(dashboard.ConfigPath())
|
||||
require.NoError(t, err)
|
||||
logData, err := os.ReadFile(dashboard.LogPath())
|
||||
require.NoError(t, err)
|
||||
jwtToken := requireJWTSignedWithDeterministicSecret(t, dashboard.Clients().REST)
|
||||
unauthenticatedStatus, unauthenticatedResponse := requestUnauthenticatedInventory(t, dashboard)
|
||||
|
||||
// Then
|
||||
require.Equal(t, int64(1), userCount)
|
||||
require.Equal(t, int64(1), tokenCount)
|
||||
require.True(t, bootstrap.LoginAuthenticated)
|
||||
require.True(t, bootstrap.CSRFCookiePresent)
|
||||
require.NotZero(t, bootstrap.PATID)
|
||||
require.Equal(t, []string{"nezha:*"}, bootstrap.PATScopes)
|
||||
require.Equal(t, "2024-11-05", bootstrap.MCPProtocolVersion)
|
||||
require.Equal(t, "nezha-mcp", bootstrap.MCPServerName)
|
||||
require.Positive(t, bootstrap.MCPToolCount)
|
||||
require.Equal(t, http.StatusOK, unauthenticatedStatus)
|
||||
require.False(t, unauthenticatedResponse.Success)
|
||||
require.Contains(t, unauthenticatedResponse.Error, "Unauthorized")
|
||||
require.Len(t, agentSecret, 32)
|
||||
require.Contains(t, string(configData), "force_auth: true")
|
||||
require.Contains(t, string(configData), "enable_mcp: true")
|
||||
require.Contains(t, string(configData), "agent_secret_key: \""+agentSecret+"\"")
|
||||
require.NotContains(t, string(configData), jwtSecret)
|
||||
require.NotContains(t, string(logData), jwtSecret)
|
||||
require.NotContains(t, string(logData), agentSecret)
|
||||
require.NotContains(t, string(logData), jwtToken)
|
||||
require.NotContains(t, string(logData), "nzp_")
|
||||
require.NotEqual(t, dashboard.ConfigPath(), dashboard.DatabasePath())
|
||||
require.FileExists(t, dashboard.DatabasePath())
|
||||
require.NotNil(t, dashboard.Clients().REST)
|
||||
require.NotNil(t, dashboard.Clients().MCP)
|
||||
require.NotNil(t, dashboard.Clients().WebSocket)
|
||||
}
|
||||
|
||||
func TestDashboard_ServesTrustedTLS(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboard(t, true)
|
||||
bootstrap := dashboard.Bootstrap()
|
||||
|
||||
// When
|
||||
wrongHostClient, wrongHostTransport, err := dashboard.newTLSHTTPClient("wronghost.invalid")
|
||||
require.NoError(t, err)
|
||||
defer wrongHostTransport.CloseIdleConnections()
|
||||
request, err := http.NewRequestWithContext(t.Context(), http.MethodPost, dashboard.TLSURL()+"/api/v1/login", strings.NewReader(`{"username":"admin","password":"admin"}`))
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
_, err = wrongHostClient.Do(request)
|
||||
|
||||
// Then
|
||||
require.True(t, bootstrap.TLSAuthenticated)
|
||||
require.False(t, dashboard.tlsFixture.ClientConfig("localhost").InsecureSkipVerify)
|
||||
var hostnameError x509.HostnameError
|
||||
require.ErrorAs(t, err, &hostnameError)
|
||||
}
|
||||
|
||||
func TestDashboard_RejectsWrongLogin(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboard(t, false)
|
||||
|
||||
// When
|
||||
_, err := dashboard.Clients().REST.Login(t.Context(), client.LoginRequest{Username: "admin", Password: "wrong-password"})
|
||||
|
||||
// Then
|
||||
require.ErrorIs(t, err, client.ErrSemanticFailure)
|
||||
require.ErrorContains(t, err, "Unauthorized")
|
||||
}
|
||||
|
||||
func TestDashboard_RejectsMalformedCSRF(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboard(t, false)
|
||||
|
||||
// When
|
||||
status, responseBody := postMalformedCSRF(t, dashboard)
|
||||
|
||||
// Then
|
||||
require.Equal(t, http.StatusForbidden, status)
|
||||
var envelope client.CommonResponse[json.RawMessage]
|
||||
require.NoError(t, json.Unmarshal(responseBody, &envelope))
|
||||
require.False(t, envelope.Success)
|
||||
require.Contains(t, envelope.Error, "invalid CSRF token")
|
||||
}
|
||||
|
||||
func TestDashboard_StopsCleanly(t *testing.T) {
|
||||
// Given
|
||||
dashboard := startDashboardWithoutCleanup(t, false)
|
||||
root := dashboard.WorkspaceRoot()
|
||||
pid := dashboard.PID()
|
||||
|
||||
// When
|
||||
stopContext, cancel := context.WithTimeout(t.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, dashboard.Stop(stopContext))
|
||||
|
||||
// Then
|
||||
receipt := dashboard.CleanupReceipt()
|
||||
require.True(t, receipt.Passed)
|
||||
require.False(t, receipt.Forced)
|
||||
require.Len(t, receipt.Processes, 1)
|
||||
require.NoDirExists(t, root)
|
||||
require.NoFileExists(t, filepath.Join("/proc", strconv.Itoa(pid)))
|
||||
}
|
||||
|
||||
func startDashboard(t *testing.T, enableTLS bool) *Dashboard {
|
||||
t.Helper()
|
||||
dashboard := startDashboardWithoutCleanup(t, enableTLS)
|
||||
t.Cleanup(func() {
|
||||
stopContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, dashboard.Stop(stopContext))
|
||||
})
|
||||
return dashboard
|
||||
}
|
||||
|
||||
func startDashboardWithoutCleanup(t *testing.T, enableTLS bool) *Dashboard {
|
||||
t.Helper()
|
||||
sourceDir, err := testpaths.NezhaSource(t.Name())
|
||||
require.NoError(t, err)
|
||||
dashboard, err := Start(t.Context(), StartConfig{SourceDir: sourceDir, EnableTLS: enableTLS})
|
||||
require.NoError(t, err)
|
||||
return dashboard
|
||||
}
|
||||
|
||||
func postMalformedCSRF(t *testing.T, dashboard *Dashboard) (int, []byte) {
|
||||
t.Helper()
|
||||
requestBody, err := json.Marshal(patRequest{Name: "malformed-csrf", Scopes: []string{"nezha:*"}})
|
||||
require.NoError(t, err)
|
||||
request, err := http.NewRequestWithContext(t.Context(), http.MethodPost, dashboard.URL()+"/api/v1/api-tokens", bytes.NewReader(requestBody))
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-CSRF-Token", "malformed")
|
||||
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)
|
||||
return response.StatusCode, responseBody
|
||||
}
|
||||
|
||||
func requireJWTSignedWithDeterministicSecret(t *testing.T, restClient *client.Client) string {
|
||||
t.Helper()
|
||||
login, err := restClient.Login(t.Context(), client.LoginRequest{Username: "admin", Password: "admin"})
|
||||
require.NoError(t, err)
|
||||
parsed, err := jwt.Parse(login.Token, func(token *jwt.Token) (any, error) {
|
||||
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, errors.New("unexpected JWT algorithm")
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed.Valid)
|
||||
return login.Token
|
||||
}
|
||||
|
||||
func requestUnauthenticatedInventory(t *testing.T, dashboard *Dashboard) (int, client.CommonResponse[json.RawMessage]) {
|
||||
t.Helper()
|
||||
transport := &http.Transport{DialContext: dialAddress(dashboard.httpAddress)}
|
||||
defer transport.CloseIdleConnections()
|
||||
httpClient := &http.Client{Transport: transport, Timeout: dashboardHTTPClientTimeout}
|
||||
request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, dashboard.URL()+"/api/v1/server", nil)
|
||||
require.NoError(t, err)
|
||||
response, err := httpClient.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))
|
||||
return response.StatusCode, envelope
|
||||
}
|
||||
Reference in New Issue
Block a user