internal/server/node_local_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"strings"
"testing"
"github.com/X4Applegate/caddyui/internal/models"
)
// v2.33.0: node-local resources.
//
// Fleet sync copies a proxy host's upstream verbatim to the target node. That
// is correct for edge replication — several Caddy nodes fronting the same
// backends — and wrong for federated nodes, where each Caddy fronts its own
// local stack and the upstream is a Docker service name or a VPN address that
// only resolves on the node that owns it. Marking a resource node-local keeps
// it on its node.
func TestFleetSyncSkipsNodeLocalResources(t *testing.T) {
s, sourceServerID, targetServerID := newFleetSyncTestServer(t)
// One host that should travel, one pinned to this node.
shared := &models.ProxyHost{
Domains: "shared.example.com", ForwardScheme: "https",
ForwardHost: "origin.example.com", ForwardPort: 443, Enabled: true,
}
if _, err := models.CreateProxyHost(s.DB, sourceServerID, 0, shared); err != nil {
t.Fatal(err)
}
local := &models.ProxyHost{
Domains: "photos.example.com", ForwardScheme: "http",
ForwardHost: "immich_server", ForwardPort: 2283, Enabled: true, NodeLocal: true,
}
if _, err := models.CreateProxyHost(s.DB, sourceServerID, 0, local); err != nil {
t.Fatal(err)
}
localRoute := &models.RawRoute{
Label: "local-route", JSONData: `{"handle":[]}`, Enabled: true, NodeLocal: true,
}
if _, err := models.CreateRawRoute(s.DB, sourceServerID, 0, localRoute); err != nil {
t.Fatal(err)
}
summary, err := s.syncFleetConfiguration("tester", sourceServerID, targetServerID)
if err != nil {
t.Fatal(err)
}
if summary.ProxiesCreated != 1 {
t.Errorf("ProxiesCreated = %d, want 1 (only the shared host should travel)", summary.ProxiesCreated)
}
if summary.ProxiesSkipped != 1 {
t.Errorf("ProxiesSkipped = %d, want 1", summary.ProxiesSkipped)
}
if summary.RawRoutesCreated != 0 {
t.Errorf("RawRoutesCreated = %d, want 0", summary.RawRoutesCreated)
}
if summary.RawRoutesSkipped != 1 {
t.Errorf("RawRoutesSkipped = %d, want 1", summary.RawRoutesSkipped)
}
// The target must hold the shared host and nothing node-local — the whole
// point is that immich_server never appears on a node that can't resolve it.
targets, err := models.ListProxyHosts(s.DB, targetServerID, 0, true, nil)
if err != nil {
t.Fatal(err)
}
if len(targets) != 1 {
t.Fatalf("target has %d proxy hosts, want 1", len(targets))
}
if targets[0].Domains != "shared.example.com" {
t.Errorf("target host = %q, want shared.example.com", targets[0].Domains)
}
for _, h := range targets {
if h.ForwardHost == "immich_server" {
t.Error("a node-local host reached the target node")
}
}
targetRoutes, err := models.ListRawRoutes(s.DB, targetServerID, 0, true, nil)
if err != nil {
t.Fatal(err)
}
if len(targetRoutes) != 0 {
t.Errorf("target has %d advanced routes, want 0", len(targetRoutes))
}
}
// Skipped resources must be reported. An operator reading "1 added" needs to
// know another was deliberately left behind, not silently lost.
func TestFleetSyncSummaryReportsSkips(t *testing.T) {
withSkips := fleetSyncSummary{ProxiesCreated: 1, ProxiesSkipped: 3, RawRoutesSkipped: 2}
got := withSkips.String()
if !strings.Contains(got, "skipped as node-local: 3 proxies, 2 advanced routes") {
t.Errorf("summary does not report skips: %s", got)
}
// No skips, no noise.
clean := fleetSyncSummary{ProxiesCreated: 1}
if strings.Contains(clean.String(), "skipped as node-local") {
t.Errorf("summary mentions skips when there were none: %s", clean.String())
}
}
// "Also deploy to" must refuse a node-local resource too, otherwise the
// per-host form becomes a way around the flag.
func TestCrossDeployRefusesNodeLocal(t *testing.T) {
s, sourceServerID, targetServerID := newFleetSyncTestServer(t)
local := &models.ProxyHost{
Domains: "photos.example.com", ForwardScheme: "http",
ForwardHost: "immich_server", ForwardPort: 2283, Enabled: true, NodeLocal: true,
}
id, err := models.CreateProxyHost(s.DB, sourceServerID, 0, local)
if err != nil {
t.Fatal(err)
}
local.ID = id
s.crossDeployProxyHost("tester", sourceServerID, local, []int64{targetServerID})
targets, err := models.ListProxyHosts(s.DB, targetServerID, 0, true, nil)
if err != nil {
t.Fatal(err)
}
if len(targets) != 0 {
t.Errorf("cross-deploy pushed a node-local host: %d row(s) on target", len(targets))
}
rr := &models.RawRoute{Label: "local", JSONData: `{"handle":[]}`, Enabled: true, NodeLocal: true}
rrID, err := models.CreateRawRoute(s.DB, sourceServerID, 0, rr)
if err != nil {
t.Fatal(err)
}
rr.ID = rrID
s.crossDeployRawRoute("tester", sourceServerID, rr, []int64{targetServerID})
targetRoutes, err := models.ListRawRoutes(s.DB, targetServerID, 0, true, nil)
if err != nil {
t.Fatal(err)
}
if len(targetRoutes) != 0 {
t.Errorf("cross-deploy pushed a node-local advanced route: %d row(s) on target", len(targetRoutes))
}
}
// A host that isn't marked node-local must still sync — the flag is opt-in and
// must not change behaviour for anyone who never sets it.
func TestFleetSyncStillCopiesNormalHosts(t *testing.T) {
s, sourceServerID, targetServerID := newFleetSyncTestServer(t)
normal := &models.ProxyHost{
Domains: "app.example.com", ForwardScheme: "https",
ForwardHost: "origin.example.com", ForwardPort: 443, Enabled: true,
}
if _, err := models.CreateProxyHost(s.DB, sourceServerID, 0, normal); err != nil {
t.Fatal(err)
}
summary, err := s.syncFleetConfiguration("tester", sourceServerID, targetServerID)
if err != nil {
t.Fatal(err)
}
if summary.ProxiesCreated != 1 || summary.ProxiesSkipped != 0 {
t.Errorf("created=%d skipped=%d, want 1/0", summary.ProxiesCreated, summary.ProxiesSkipped)
}
}