Files
vpp-maglev/internal/prober/http_test.go

232 lines
5.6 KiB
Go

// SPDX-License-Identifier: Apache-2.0
package prober
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"regexp"
"testing"
"time"
"git.ipng.ch/ipng/vpp-maglev/internal/config"
)
// dialAndProbe dials addr directly (bypassing netns/interface binding) and
// exercises the HTTP probe response-checking logic.
func dialAndProbe(ctx context.Context, addr string, cfg ProbeConfig) (bool, error) {
if cfg.HTTP == nil {
return false, fmt.Errorf("dialAndProbe requires HTTP params")
}
p := cfg.HTTP
path := p.Path
if path == "" {
path = "/"
}
conn, err := net.DialTimeout("tcp", addr, cfg.Timeout)
if err != nil {
return false, err
}
transport := &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return conn, nil
},
DisableKeepAlives: true,
}
client := &http.Client{
Transport: transport,
Timeout: cfg.Timeout,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
target := "http://" + addr + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return false, err
}
if p.Host != "" {
req.Host = p.Host
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < p.ResponseCodeMin || resp.StatusCode > p.ResponseCodeMax {
return false, nil
}
if p.ResponseRegexp != nil {
body, _ := io.ReadAll(resp.Body)
if !p.ResponseRegexp.Match(body) {
return false, nil
}
}
return true, nil
}
func TestHTTPProbeStatusCode(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprint(w, "healthy")
}))
defer srv.Close()
cfg := ProbeConfig{
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/healthz",
ResponseCodeMin: 200,
ResponseCodeMax: 200,
},
}
ok, err := dialAndProbe(context.Background(), srv.Listener.Addr().String(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected probe success")
}
}
func TestHTTPProbeWrongStatusCode(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer srv.Close()
cfg := ProbeConfig{
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/",
ResponseCodeMin: 200,
ResponseCodeMax: 200,
},
}
ok, err := dialAndProbe(context.Background(), srv.Listener.Addr().String(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("expected probe failure on wrong status code")
}
}
func TestHTTPProbeRegexpMatch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = fmt.Fprint(w, `{"status":"ok"}`)
}))
defer srv.Close()
cfg := ProbeConfig{
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/",
ResponseCodeMin: 200,
ResponseCodeMax: 200,
ResponseRegexp: regexp.MustCompile(`"status":"ok"`),
},
}
ok, err := dialAndProbe(context.Background(), srv.Listener.Addr().String(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected probe success with matching regexp")
}
}
func TestHTTPProbeRegexpNoMatch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = fmt.Fprint(w, `{"status":"degraded"}`)
}))
defer srv.Close()
cfg := ProbeConfig{
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/",
ResponseCodeMin: 200,
ResponseCodeMax: 200,
ResponseRegexp: regexp.MustCompile(`"status":"ok"`),
},
}
ok, err := dialAndProbe(context.Background(), srv.Listener.Addr().String(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("expected probe failure when regexp does not match")
}
}
func TestHTTPSProbe(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
host, portStr, _ := net.SplitHostPort(srv.Listener.Addr().String())
port := uint16(0)
_, _ = fmt.Sscanf(portStr, "%d", &port)
cfg := ProbeConfig{
Target: net.ParseIP(host),
Port: port,
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/",
ResponseCodeMin: 200,
ResponseCodeMax: 200,
InsecureSkipVerify: true,
},
}
// Verify HTTPSProbe succeeds (TLS conn reused, no double-wrap).
result := HTTPSProbe(context.Background(), cfg)
if !result.OK {
t.Errorf("HTTPSProbe failed: code=%s detail=%s", result.Code, result.Detail)
}
// Verify HTTPProbe (plain) against the TLS server fails at the TLS layer,
// not with a double-TLS confusion error.
result = HTTPProbe(context.Background(), cfg)
if result.OK {
t.Error("plain HTTPProbe against TLS server should fail")
}
}
func TestHTTPProbeNoRedirect(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/other", http.StatusFound)
}))
defer srv.Close()
// Probe expects 302 — redirect is not followed.
cfg := ProbeConfig{
Timeout: 2 * time.Second,
HTTP: &config.HTTPParams{
Path: "/",
ResponseCodeMin: 302,
ResponseCodeMax: 302,
},
}
ok, err := dialAndProbe(context.Background(), srv.Listener.Addr().String(), cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected success when probe expects 302 and server returns 302")
}
}