1package container
2
3import (
4 "context"
5 "net"
6 "net/http"
7 "net/http/httptest"
8 "sync/atomic"
9 "syscall"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17// --- TCP Probe Tests ---
18
19func TestTCPProbe_Success(t *testing.T) {
20 ln, err := net.Listen("tcp", "127.0.0.1:0")
21 require.NoError(t, err)
22 defer ln.Close()
23
24 // Accept connections in background so the probe succeeds
25 go func() {
26 for {
27 conn, err := ln.Accept()
28 if err != nil {
29 return
30 }
31 conn.Close()
32 }
33 }()
34
35 ctx := testContext()
36 err = tcpProbe(ctx, ln.Addr().String(), 5*time.Second)
37 assert.NoError(t, err)
38}
39
40func TestTCPProbe_Timeout(t *testing.T) {
41 // Reserve an ephemeral port and close it immediately to get a deterministic
42 // unreachable target, avoiding assumptions about privileged ports.
43 ln, err := net.Listen("tcp", "127.0.0.1:0")
44 require.NoError(t, err)
45 addr := ln.Addr().String()
46 ln.Close()
47
48 ctx := testContext()
49 err = tcpProbe(ctx, addr, 200*time.Millisecond)
50 assert.Error(t, err)
51 assert.Contains(t, err.Error(), "TCP probe timeout")
52}
53
54func TestTCPProbe_ContextCancelled(t *testing.T) {
55 ctx, cancel := context.WithCancel(testContext())
56 cancel()
57 err := tcpProbe(ctx, "127.0.0.1:1", 5*time.Second)
58 assert.Error(t, err)
59}
60
61func TestTCPProbe_DelayedListener(t *testing.T) {
62 ln, err := net.Listen("tcp", "127.0.0.1:0")
63 require.NoError(t, err)
64 // Close initially so probe fails at first
65 addr := ln.Addr().String()
66 ln.Close()
67
68 // Re-open after 1 second
69 go func() {
70 time.Sleep(time.Second)
71 lc := net.ListenConfig{
72 Control: func(network, address string, c syscall.RawConn) error {
73 return c.Control(func(fd uintptr) {
74 _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
75 })
76 },
77 }
78 newLn, err := lc.Listen(context.Background(), "tcp", addr)
79 if err != nil {
80 t.Logf("delayed listener failed to bind: %v", err)
81 return
82 }
83 defer newLn.Close()
84 for {
85 conn, err := newLn.Accept()
86 if err != nil {
87 return
88 }
89 conn.Close()
90 }
91 }()
92
93 ctx := testContext()
94 err = tcpProbe(ctx, addr, 5*time.Second)
95 assert.NoError(t, err)
96}
97
98// --- HTTP Probe Tests ---
99
100func TestHTTPProbe_Success(t *testing.T) {
101 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102 if r.URL.Path == "/healthz" {
103 w.WriteHeader(200)
104 return
105 }
106 w.WriteHeader(404)
107 }))
108 defer srv.Close()
109
110 ctx := testContext()
111 err := httpProbe(ctx, srv.URL+"/healthz", 5*time.Second)
112 assert.NoError(t, err)
113}
114
115func TestHTTPProbe_ServerError_ThenSuccess(t *testing.T) {
116 var calls int32
117 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 n := atomic.AddInt32(&calls, 1)
119 if n < 3 {
120 w.WriteHeader(503)
121 return
122 }
123 w.WriteHeader(200)
124 }))
125 defer srv.Close()
126
127 ctx := testContext()
128 err := httpProbe(ctx, srv.URL+"/healthz", 10*time.Second)
129 assert.NoError(t, err)
130 assert.GreaterOrEqual(t, atomic.LoadInt32(&calls), int32(3))
131}
132
133func TestHTTPProbe_Timeout(t *testing.T) {
134 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135 w.WriteHeader(503)
136 }))
137 defer srv.Close()
138
139 ctx := testContext()
140 err := httpProbe(ctx, srv.URL+"/healthz", 500*time.Millisecond)
141 assert.Error(t, err)
142 assert.Contains(t, err.Error(), "HTTP probe timeout")
143}
144
145func TestHTTPProbe_ContextCancelled(t *testing.T) {
146 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
147 w.WriteHeader(503)
148 }))
149 defer srv.Close()
150
151 ctx, cancel := context.WithCancel(testContext())
152 cancel()
153 err := httpProbe(ctx, srv.URL+"/healthz", 5*time.Second)
154 assert.Error(t, err)
155}
156
157func TestHTTPProbe_RedirectIsSuccess(t *testing.T) {
158 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159 http.Redirect(w, r, "/other", http.StatusFound)
160 }))
161 defer srv.Close()
162
163 ctx := testContext()
164 // The probe should treat 3xx as success without following the redirect
165 err := httpProbe(ctx, srv.URL+"/healthz", 5*time.Second)
166 assert.NoError(t, err)
167}