Files
kandev/apps/backend/internal/agent/docker/client_test.go
T
José Almeida 32c6cd5bc9 refactor(docker): split reconnectToContainer + cover reconnect helpers with unit tests
Address recurring review feedback on the docker reconnect path:

- reconnectToContainer was 98 lines (over the 80-line repo limit). Split
  into ensureContainerRunning (inspect + start + re-inspect) and
  bringupAgentctl (health-check, find-or-create instance, transparent
  re-handshake). The orchestration body now reads top-down at 49 lines.
- resolveDockerEndpoint and waitForAgentctlHealth grew real branches in
  this PR (host-port lookup with container-IP fallback; 240×500ms
  retry loop) but had no direct tests. Introduce narrow hostPortLookup
  / healthChecker interfaces, satisfied by *docker.Client and
  *agentctl.ControlClient, so both functions are now driven by stubs
  that exercise happy-path, fallback, retry-until-healthy, exhaustion,
  and context-cancellation branches.
- Add direct unit tests for the docker package's pure helpers
  (normalizeDockerHostIP, buildDockerPortBindings) and for
  dockerAgentctlPortBindings — a regression in the published port
  range or host IP would silently break container reconnect.
2026-05-07 12:16:13 +01:00

77 lines
2.1 KiB
Go

package docker
import (
"fmt"
"testing"
"github.com/docker/go-connections/nat"
)
func TestNormalizeDockerHostIP(t *testing.T) {
cases := []struct {
in string
want string
}{
{in: "", want: "127.0.0.1"},
{in: "0.0.0.0", want: "127.0.0.1"},
{in: "::", want: "127.0.0.1"},
{in: "127.0.0.1", want: "127.0.0.1"},
{in: "10.0.0.5", want: "10.0.0.5"},
{in: "::1", want: "::1"},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
if got := normalizeDockerHostIP(tc.in); got != tc.want {
t.Errorf("normalizeDockerHostIP(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestBuildDockerPortBindings_EmptyReturnsNil(t *testing.T) {
exposed, bindings := buildDockerPortBindings(nil)
if exposed != nil {
t.Errorf("expected nil exposed ports, got %v", exposed)
}
if bindings != nil {
t.Errorf("expected nil bindings, got %v", bindings)
}
}
func TestBuildDockerPortBindings_AssignsContainerAndHost(t *testing.T) {
in := []PortBindingConfig{
{ContainerPort: 8080, HostIP: "127.0.0.1", HostPort: "0"},
{ContainerPort: 9000, HostIP: "0.0.0.0", HostPort: "9001"},
}
exposed, bindings := buildDockerPortBindings(in)
if got := len(exposed); got != 2 {
t.Fatalf("exposed ports = %d, want 2", got)
}
for _, b := range in {
key := nat.Port(fmt.Sprintf("%d/tcp", b.ContainerPort))
if _, ok := exposed[key]; !ok {
t.Errorf("exposed missing %s", key)
}
got := bindings[key]
if len(got) != 1 {
t.Fatalf("bindings[%s] = %d entries, want 1", key, len(got))
}
if got[0].HostIP != b.HostIP || got[0].HostPort != b.HostPort {
t.Errorf("bindings[%s] = %+v, want host_ip=%q host_port=%q", key, got[0], b.HostIP, b.HostPort)
}
}
}
func TestBuildDockerPortBindings_DeduplicatesContainerPort(t *testing.T) {
in := []PortBindingConfig{
{ContainerPort: 7000, HostIP: "127.0.0.1", HostPort: "0"},
{ContainerPort: 7000, HostIP: "10.0.0.5", HostPort: "7000"},
}
_, bindings := buildDockerPortBindings(in)
got := bindings[nat.Port("7000/tcp")]
if len(got) != 2 {
t.Fatalf("want both bindings on port 7000, got %d", len(got))
}
}