1. per-client links (LinkProvider.LinksForClient) now apply the inbound's managed Host records with the same precedence as subscriptions and export-all (host endpoints after fallback projection); zero-host inbounds keep the legacy output. 2. agent node writes no longer store the form placeholder address='agent', port=2053: CreateFromRequest/UpdateFromRequest sanitize it to the node's resolved agent address (update) or empty (create / unreported), port 0; normalize gains a lenient endpoint mode for agent writes only. 3. runtime Remote.baseURL refuses the legacy 'agent' placeholder with a clear no-panel-endpoint error instead of splicing it into a dialable https://agent:2053 URL.
153 lines
5.0 KiB
Go
153 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// Agent nodes have no panel endpoint: the node form submits the
|
|
// placeholder pair address="agent", port=2053 for them. The write paths
|
|
// must never store that pair — an agent node's row carries its resolved
|
|
// address when one is known, or empty when it is not — while traditional
|
|
// nodes keep the strict endpoint validation they always had.
|
|
|
|
func strPtr(s string) *string { return &s }
|
|
|
|
func TestCreateFromRequest_AgentPlaceholderNotStored(t *testing.T) {
|
|
setupConflictDB(t)
|
|
svc := &NodeService{}
|
|
|
|
view, err := svc.CreateFromRequest(&NodeMutationRequest{
|
|
Name: "agent-new", Enable: true, Scheme: "https",
|
|
Address: "agent", Port: 2053, NodeType: "agent",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateFromRequest(agent): %v", err)
|
|
}
|
|
if view.Address == "agent" || view.Port == 2053 {
|
|
t.Fatalf("create view still carries the placeholder: %+v", view)
|
|
}
|
|
stored, err := svc.GetById(view.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetById: %v", err)
|
|
}
|
|
// A brand-new agent node has no binding, hence no resolved address:
|
|
// the honest stored value is empty, port 0.
|
|
if stored.Address != "" || stored.Port != 0 {
|
|
t.Fatalf("stored agent endpoint = %q:%d, want empty:0", stored.Address, stored.Port)
|
|
}
|
|
}
|
|
|
|
func TestCreateFromRequest_TraditionalKeepsEndpoint(t *testing.T) {
|
|
setupConflictDB(t)
|
|
svc := &NodeService{}
|
|
|
|
view, err := svc.CreateFromRequest(&NodeMutationRequest{
|
|
Name: "trad-new", Enable: true, Scheme: "https",
|
|
Address: "198.51.100.10", Port: 8443, ApiToken: strPtr("tok-1"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateFromRequest(traditional): %v", err)
|
|
}
|
|
stored, err := svc.GetById(view.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetById: %v", err)
|
|
}
|
|
if stored.Address != "198.51.100.10" || stored.Port != 8443 {
|
|
t.Fatalf("stored traditional endpoint = %q:%d, want 198.51.100.10:8443", stored.Address, stored.Port)
|
|
}
|
|
}
|
|
|
|
func TestCreateFromRequest_TraditionalEmptyAddressStillRejected(t *testing.T) {
|
|
setupConflictDB(t)
|
|
svc := &NodeService{}
|
|
|
|
if _, err := svc.CreateFromRequest(&NodeMutationRequest{
|
|
Name: "trad-bad", Enable: true, Scheme: "https", ApiToken: strPtr("tok-2"),
|
|
}); err == nil {
|
|
t.Fatal("traditional create with an empty address must still fail validation")
|
|
}
|
|
}
|
|
|
|
// seedAgentNodeRow stores a legacy placeholder row plus its agent
|
|
// binding; ipv4 == "" models an agent that never reported an address.
|
|
func seedAgentNodeRow(t *testing.T, name, ipv4 string) *model.Node {
|
|
t.Helper()
|
|
db := database.GetDB()
|
|
node := &model.Node{Name: name, Scheme: "https", Address: "agent", Port: 2053, Enable: true}
|
|
if err := db.Create(node).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
ag := &model.AgentNode{TokenHash: "hash-" + name, Name: name, NodeID: &node.Id, IPv4: ipv4, LastSeen: 100}
|
|
if err := db.Create(ag).Error; err != nil {
|
|
t.Fatalf("create binding: %v", err)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func TestUpdateFromRequest_AgentPlaceholderReplacedByResolved(t *testing.T) {
|
|
setupConflictDB(t)
|
|
node := seedAgentNodeRow(t, "agent-upd", "203.0.113.9")
|
|
svc := &NodeService{}
|
|
|
|
// Exactly what the edit form submits for an agent node today.
|
|
err := svc.UpdateFromRequest(node.Id, &NodeMutationRequest{
|
|
Name: "agent-upd", Enable: true, Scheme: "https",
|
|
Address: "agent", Port: 2053, NodeType: "agent",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateFromRequest(agent): %v", err)
|
|
}
|
|
stored, err := svc.GetById(node.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetById: %v", err)
|
|
}
|
|
if stored.Address != "203.0.113.9" || stored.Port != 0 {
|
|
t.Fatalf("stored agent endpoint = %q:%d, want resolved 203.0.113.9:0", stored.Address, stored.Port)
|
|
}
|
|
}
|
|
|
|
func TestUpdateFromRequest_AgentNoReport_SanitizesToEmpty(t *testing.T) {
|
|
setupConflictDB(t)
|
|
node := seedAgentNodeRow(t, "agent-silent", "")
|
|
svc := &NodeService{}
|
|
|
|
err := svc.UpdateFromRequest(node.Id, &NodeMutationRequest{
|
|
Name: "agent-silent", Enable: true, Scheme: "https",
|
|
Address: "agent", Port: 2053, NodeType: "agent",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateFromRequest(agent): %v", err)
|
|
}
|
|
stored, err := svc.GetById(node.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetById: %v", err)
|
|
}
|
|
if stored.Address != "" || stored.Port != 0 {
|
|
t.Fatalf("stored agent endpoint = %q:%d, want empty:0 (no placeholder, no invented address)", stored.Address, stored.Port)
|
|
}
|
|
}
|
|
|
|
func TestUpdateFromRequest_AgentRealAddressKeptVerbatim(t *testing.T) {
|
|
setupConflictDB(t)
|
|
node := seedAgentNodeRow(t, "agent-real", "203.0.113.9")
|
|
svc := &NodeService{}
|
|
|
|
err := svc.UpdateFromRequest(node.Id, &NodeMutationRequest{
|
|
Name: "agent-real", Enable: true, Scheme: "https",
|
|
Address: "edge.example.com", Port: 443, NodeType: "agent",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateFromRequest(agent, real address): %v", err)
|
|
}
|
|
stored, err := svc.GetById(node.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetById: %v", err)
|
|
}
|
|
if stored.Address != "edge.example.com" || stored.Port != 443 {
|
|
t.Fatalf("stored agent endpoint = %q:%d, want submitted edge.example.com:443", stored.Address, stored.Port)
|
|
}
|
|
}
|