1package deployment
2
3import (
4 "context"
5 "net"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/gordon/internal/domain"
15)
16
17// TestWaitHTTPReady_ProbeURLKeepsLoopbackAuthority proves the probe URL is
18// built from an immutable loopback host and an escaped path, so no declared
19// path can redirect the request to a different authority. The Host header
20// must name the declared container port, not the random published port.
21func TestWaitHTTPReady_ProbeURLKeepsLoopbackAuthority(t *testing.T) {
22 var gotURL, gotHost string
23 deps := ProbeDeps{httpGet: func(_ context.Context, url, hostAuthority string) (int, error) {
24 gotURL, gotHost = url, hostAuthority
25 return 200, nil
26 }}
27 spec := domain.AppService{
28 Name: "web",
29 HTTP: []domain.AppHTTPInterface{{Host: "web.example.com", Port: 8080}},
30 Readiness: domain.AppReadiness{Type: domain.AppReadinessHTTP, Path: "/healthz?probe=1", Timeout: time.Second},
31 }
32 require.NoError(t, waitHTTPReadyWithDeps(context.Background(), deps, spec, map[int]int{8080: 18080}))
33 assert.Equal(t, "http://127.0.0.1:18080/healthz?probe=1", gotURL)
34 assert.Equal(t, "127.0.0.1:8080", gotHost, "the Host header names the declared container port, not the published port")
35}
36
37// TestWaitHTTPReady_SendsContainerPortAsHostAuthority is the qBittorrent
38// regression: readiness dials the random loopback publication, but the
39// workload rejects a Host whose port is not its declared container port
40// with 401 "Invalid Host header, port mismatch". The probe must dial the
41// published port while sending the container port as the Host authority.
42func TestWaitHTTPReady_SendsContainerPortAsHostAuthority(t *testing.T) {
43 const containerPort = 8080
44 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45 if r.Host != "127.0.0.1:8080" {
46 http.Error(w, "Invalid Host header, port mismatch", http.StatusUnauthorized)
47 return
48 }
49 w.WriteHeader(http.StatusOK)
50 }))
51 defer server.Close()
52 publishedPort := server.Listener.Addr().(*net.TCPAddr).Port
53
54 spec := domain.AppService{
55 Name: "qbittorrent",
56 HTTP: []domain.AppHTTPInterface{{Host: "qbit.example.com", Port: containerPort}},
57 Readiness: domain.AppReadiness{Type: domain.AppReadinessHTTP, Path: "/api/v2/app/version", Timeout: 2 * time.Second},
58 }
59 require.NoError(t, waitHTTPReadyWithDeps(context.Background(), ProbeDeps{httpGet: httpGetOnce}, spec, map[int]int{containerPort: publishedPort}))
60}
61
62// TestWaitHTTPReady_HangingProbeRespectsDeadline proves a workload that
63// never answers cannot hold the readiness operation open past its bound.
64func TestWaitHTTPReady_HangingProbeRespectsDeadline(t *testing.T) {
65 deps := ProbeDeps{httpGet: func(ctx context.Context, _, _ string) (int, error) {
66 <-ctx.Done()
67 return 0, ctx.Err()
68 }}
69 spec := domain.AppService{
70 Name: "web",
71 HTTP: []domain.AppHTTPInterface{{Host: "web.example.com", Port: 8080}},
72 Readiness: domain.AppReadiness{Type: domain.AppReadinessHTTP, Path: "/healthz", Timeout: time.Second},
73 }
74 start := time.Now()
75 err := waitHTTPReadyWithDeps(context.Background(), deps, spec, map[int]int{8080: 18080})
76 require.Error(t, err)
77 assert.Less(t, time.Since(start), 6*time.Second)
78}