2a5b88fbf9
agentClient() built a new agentapi.Client (new bare http.Transport, IdleConnTimeout:0) per call and discarded it without closing idle conns -> one leaked idle ESTABLISHED socket per call to the agent :8443, exhausting the ephemeral port range after ~5 days (EADDRNOTAVAIL). Memoize one shared client via sync.Once; harden Transport (MaxIdleConns/PerHost + IdleConnTimeout 90s). Agent/firewall untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
1.7 KiB
Go
48 lines
1.7 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
)
|
|
|
|
// T1: agentClient() must return the SAME *agentapi.Client across calls — one Client ⇒ one pooled
|
|
// http.Transport ⇒ the per-call Transport leak (idle ESTABLISHED sockets to the agent's :8443 that
|
|
// exhausted the ephemeral port range after ~5 days) is structurally impossible. cfg.LocalAPI is
|
|
// valid-but-non-network (agentapi.New builds the struct without dialing), so this is hermetic.
|
|
//
|
|
// Companion red-proof: reverting agentClient() to `return agentapi.New(...)` makes the two pointers
|
|
// differ → this test fails (demonstrated, then reverted, during implementation).
|
|
func TestAgentClient_ReusesSameInstance(t *testing.T) {
|
|
s := &Server{cfg: &config.Config{LocalAPI: config.LocalAPIConfig{
|
|
Endpoint: "127.0.0.1:8443",
|
|
Token: "tok",
|
|
Fingerprint: strings.Repeat("a", 64), // syntactically valid 64-hex; no network touched
|
|
}}}
|
|
|
|
c1, err := s.agentClient()
|
|
if err != nil {
|
|
t.Fatalf("agentClient() #1: %v", err)
|
|
}
|
|
c2, err := s.agentClient()
|
|
if err != nil {
|
|
t.Fatalf("agentClient() #2: %v", err)
|
|
}
|
|
if c1 == nil || c2 == nil {
|
|
t.Fatal("agentClient() returned a nil client")
|
|
}
|
|
if c1 != c2 {
|
|
t.Fatalf("agentClient() must return the SAME instance (leak fix); got %p then %p", c1, c2)
|
|
}
|
|
}
|
|
|
|
// T1b: the "not configured" semantics are preserved — an empty endpoint still errors, OUTSIDE the
|
|
// memoizing Once (so an unprovisioned guest never caches a client).
|
|
func TestAgentClient_UnconfiguredErrors(t *testing.T) {
|
|
s := &Server{cfg: &config.Config{}} // LocalAPI.Endpoint == ""
|
|
if _, err := s.agentClient(); err == nil {
|
|
t.Fatal("expected an 'agent not configured' error when Endpoint is empty")
|
|
}
|
|
}
|