- 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
127 lines
3.3 KiB
Go
127 lines
3.3 KiB
Go
package job
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
func TestAgentHeartbeatStale(t *testing.T) {
|
|
now := time.Unix(1_700_000_000, 0)
|
|
ms := func(d time.Duration) int64 { return now.Add(-d).UnixMilli() }
|
|
cases := []struct {
|
|
name string
|
|
lastSeenMs int64
|
|
want bool
|
|
}{
|
|
{"never checked in", 0, false},
|
|
{"negative stamp", -1, false},
|
|
{"fresh heartbeat", ms(60 * time.Second), false},
|
|
{"two missed heartbeats", ms(179 * time.Second), false},
|
|
{"past threshold", ms(181 * time.Second), true},
|
|
{"long gone", ms(24 * time.Hour), true},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := agentHeartbeatStale(tc.lastSeenMs, now); got != tc.want {
|
|
t.Errorf("%s: agentHeartbeatStale(%d) = %v, want %v", tc.name, tc.lastSeenMs, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedAgentNode(t *testing.T, name, status string, lastSeenMs int64) *model.Node {
|
|
t.Helper()
|
|
db := database.GetDB()
|
|
n := &model.Node{
|
|
Name: name,
|
|
Address: "agent",
|
|
Port: 2053,
|
|
Enable: true,
|
|
Status: status,
|
|
}
|
|
if err := db.Create(n).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
a := &model.AgentNode{
|
|
TokenHash: "hash-" + name,
|
|
Name: name,
|
|
NodeID: &n.Id,
|
|
LastSeen: lastSeenMs,
|
|
}
|
|
if err := db.Create(a).Error; err != nil {
|
|
t.Fatalf("create agent: %v", err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func nodeStatus(t *testing.T, id int) string {
|
|
t.Helper()
|
|
var n model.Node
|
|
if err := database.GetDB().Where("id = ?", id).First(&n).Error; err != nil {
|
|
t.Fatalf("reload node: %v", err)
|
|
}
|
|
return n.Status
|
|
}
|
|
|
|
func TestProbeOneExpiresStaleAgentNode(t *testing.T) {
|
|
setupIntegrationDB(t)
|
|
stale := time.Now().Add(-10 * time.Minute).UnixMilli()
|
|
n := seedAgentNode(t, "stale-agent", "online", stale)
|
|
|
|
(&NodeHeartbeatJob{}).probeOne(n)
|
|
|
|
if got := nodeStatus(t, n.Id); got != "offline" {
|
|
t.Fatalf("stale agent node status = %q, want offline", got)
|
|
}
|
|
}
|
|
|
|
func TestProbeOneKeepsFreshAgentNodeOnline(t *testing.T) {
|
|
setupIntegrationDB(t)
|
|
fresh := time.Now().Add(-30 * time.Second).UnixMilli()
|
|
n := seedAgentNode(t, "fresh-agent", "online", fresh)
|
|
|
|
(&NodeHeartbeatJob{}).probeOne(n)
|
|
|
|
if got := nodeStatus(t, n.Id); got != "online" {
|
|
t.Fatalf("fresh agent node status = %q, want online", got)
|
|
}
|
|
}
|
|
|
|
func TestProbeOneLeavesUnactivatedAgentNodeAlone(t *testing.T) {
|
|
setupIntegrationDB(t)
|
|
// Token issued, agent never checked in: last_seen = 0. The node must not
|
|
// be forced offline — it was never online/activated in the first place.
|
|
n := seedAgentNode(t, "dormant-agent", "unknown", 0)
|
|
|
|
(&NodeHeartbeatJob{}).probeOne(n)
|
|
|
|
if got := nodeStatus(t, n.Id); got != "unknown" {
|
|
t.Fatalf("unactivated agent node status = %q, want unknown (untouched)", got)
|
|
}
|
|
}
|
|
|
|
func TestProbeOnePanelNodeStillProbed(t *testing.T) {
|
|
setupIntegrationDB(t)
|
|
db := database.GetDB()
|
|
// No agent bound: the legacy probe path must still run and mark an
|
|
// unreachable panel node offline.
|
|
n := &model.Node{
|
|
Name: "panel-node",
|
|
Scheme: "http",
|
|
Address: "127.0.0.1",
|
|
Port: 1, // nothing listens here; probe fails fast
|
|
Enable: true,
|
|
Status: "online",
|
|
}
|
|
if err := db.Create(n).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
(&NodeHeartbeatJob{}).probeOne(n)
|
|
|
|
if got := nodeStatus(t, n.Id); got != "offline" {
|
|
t.Fatalf("unreachable panel node status = %q, want offline", got)
|
|
}
|
|
}
|