- nodes: port node-type (traditional/agent) picker back from web-src into the embedded frontend; backend annotates NodeView.nodeType from agent_nodes bindings; fix agent-node update path (skip reachability probe and apiToken requirement for agent-managed nodes); restore i18n keys in all 13 locales; install command now uses the Gitea mirror - jobs: expire agent-bound nodes whose agent_nodes.last_seen is older than max(3x heartbeat interval, 120s) = 180s; last_seen=0 (never registered) is left untouched; transitions reuse UpdateHeartbeat + node event broadcast - config: GetBaseVersion honors ldflags-injected buildVersion; release builds inject 3.7.0-agent.1 so the panel no longer reports a phantom update against the Gitea release tag - logger/server: remove the three log viewers (panel/xray/amneziawg), their API endpoints and the 10240-entry in-memory log buffer; file logging via lumberjack unchanged; tests assert via a new optional logger hook instead of the removed buffer - server: remove system history and xray metrics modules end to end (endpoints, sampling appends in RefreshStatus, per-minute persist cron, gob snapshot/restore, XrayMetricsService); realtime status and per-node history are untouched - settings: subPort may be 0/unset - subscription links are generated without a port while the subscription service keeps binding the default 2096; the settings form declares the cleared state via onNumber's new onClear option instead of snapping back to 1
131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
package network
|
|
|
|
import (
|
|
"errors"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
type failingListener struct{ err error }
|
|
|
|
func (l failingListener) Accept() (net.Conn, error) { return nil, l.err }
|
|
func (failingListener) Close() error { return nil }
|
|
func (failingListener) Addr() net.Addr { return testAddr("failing") }
|
|
|
|
type testAddr string
|
|
|
|
func (a testAddr) Network() string { return string(a) }
|
|
func (a testAddr) String() string { return string(a) }
|
|
|
|
// captureLogLines installs a logger hook collecting "LEVEL message" lines
|
|
// for the rest of the test. It replaces reads of the in-memory log buffer,
|
|
// which was removed together with the web log viewer.
|
|
func captureLogLines(t *testing.T) *[]string {
|
|
t.Helper()
|
|
var mu sync.Mutex
|
|
lines := &[]string{}
|
|
prev := logger.SetLogHook(func(level, msg string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
*lines = append(*lines, level+" "+msg)
|
|
})
|
|
t.Cleanup(func() { logger.SetLogHook(prev) })
|
|
return lines
|
|
}
|
|
|
|
func TestServeHTTPLogsUnexpectedListenerFailure(t *testing.T) {
|
|
errInjected := errors.New("injected listener failure")
|
|
logLines := captureLogLines(t)
|
|
ServeHTTP(&http.Server{}, failingListener{err: errInjected}, "Test server")
|
|
|
|
for _, line := range *logLines {
|
|
if strings.Contains(line, errInjected.Error()) {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("unexpected listener failure was not recorded in the panel log")
|
|
}
|
|
|
|
func TestServeHTTPSuppressesNormalServerClose(t *testing.T) {
|
|
const marker = "normal-close-must-stay-silent"
|
|
logLines := captureLogLines(t)
|
|
ServeHTTP(&http.Server{}, failingListener{err: http.ErrServerClosed}, marker)
|
|
|
|
for _, line := range *logLines {
|
|
if strings.Contains(line, marker) {
|
|
t.Fatalf("normal http.ErrServerClosed was recorded as an error: %s", line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProductionHTTPServersUseServeHTTPWrapper(t *testing.T) {
|
|
_, currentFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("locate test source")
|
|
}
|
|
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(currentFile), "../../.."))
|
|
fset := token.NewFileSet()
|
|
|
|
err := filepath.WalkDir(repoRoot, func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() {
|
|
if entry.Name() == ".git" || entry.Name() == "vendor" || entry.Name() == "node_modules" {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") || path == currentFile || path == filepath.Join(filepath.Dir(currentFile), "serve.go") {
|
|
return nil
|
|
}
|
|
|
|
parsed, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
usesHTTP := false
|
|
for _, imp := range parsed.Imports {
|
|
if imp.Path.Value == `"net/http"` {
|
|
usesHTTP = true
|
|
break
|
|
}
|
|
}
|
|
if !usesHTTP {
|
|
return nil
|
|
}
|
|
|
|
parsed, err = parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ast.Inspect(parsed, func(node ast.Node) bool {
|
|
call, ok := node.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
if ok && selector.Sel.Name == "Serve" {
|
|
position := fset.Position(call.Pos())
|
|
t.Errorf("direct Serve call at %s; production HTTP servers must use network.ServeHTTP", position)
|
|
}
|
|
return true
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("scan production Go files: %v", err)
|
|
}
|
|
}
|