package probe import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "errors" "io" "math/big" "net" "net/http" "net/http/httptest" "net/netip" "net/url" "strings" "testing" "time" ) type fakeHTTPDoer struct { response *http.Response err error request *http.Request } func (f *fakeHTTPDoer) Do(request *http.Request) (*http.Response, error) { f.request = request return f.response, f.err } func probeDefinition(probeType string) Definition { return Definition{ ID: "probe-1", ServiceID: "service-1", Name: "probe", Type: probeType, Target: Target{Scheme: "http", Host: "service.internal", Port: 8080, Path: "/health"}, Interval: 30 * time.Second, Timeout: 2 * time.Second, Enabled: true, Revision: 1, VerifyTLS: false, FollowRedirects: false, } } func executorPolicy() NetworkPolicy { return NetworkPolicy{Revision: 1, AllowedNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("127.0.0.0/8")}} } func executorResolver(_ context.Context, _ string) ([]netip.Addr, error) { return []netip.Addr{netip.MustParseAddr("10.10.0.8")}, nil } func TestProbeExecutorHTTPAssertionsAndStatus(t *testing.T) { body := `{"ok":true,"message":"ready"}` doer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}} definition := probeDefinition(TypeHTTP) definition.ContentAssertion = map[string]any{"keyword": "ready", "json": map[string]any{"ok": true}} executor := ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: doer} result, err := executor.Execute(context.Background(), definition) if err != nil { t.Fatal(err) } if result.State != "up" || result.StatusCode == nil || *result.StatusCode != http.StatusOK { t.Fatalf("unexpected HTTP result: %+v", result) } if doer.request == nil || doer.request.Method != http.MethodGet || doer.request.URL.String() != "http://service.internal:8080/health" { t.Fatalf("unexpected request: %+v", doer.request) } } func TestProbeExecutorHTTPFailureBodyLimitAndRedaction(t *testing.T) { definition := probeDefinition(TypeHTTP) definition.ExpectedStatusCodes = []int{http.StatusOK} definition.SecretReference = "secret-ref-should-never-leak" failureDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusServiceUnavailable, Body: io.NopCloser(strings.NewReader("down"))}} failure, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: failureDoer}).Execute(context.Background(), definition) if err != nil { t.Fatal(err) } if failure.State != "down" || failure.ErrorClass != "status_not_expected" || strings.Contains(failure.ErrorMessage, definition.SecretReference) { t.Fatalf("unexpected failure result: %+v", failure) } if failureDoer.request == nil || failureDoer.request.Header.Get("Authorization") != "" { t.Fatal("probe credentials were placed in the request") } limitedPolicy := executorPolicy() limitedPolicy.MaxResponseBytes = 4 largeDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("12345"))}} large, err := (ProbeExecutor{Policy: limitedPolicy, Resolver: resolverFunc(executorResolver), HTTP: largeDoer}).Execute(context.Background(), definition) if err != nil { t.Fatal(err) } if large.State != "unknown" || large.ErrorClass != "response_too_large" { t.Fatalf("unexpected bounded response result: %+v", large) } } func TestProbeExecutorHTTPRedirectPolicyAndTimeout(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.URL.Path == "/start" { http.Redirect(response, request, "/final", http.StatusFound) return } _, _ = response.Write([]byte("final")) })) defer server.Close() serverURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } port := mustPort(serverURL.Port()) resolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) { if host == "service.internal" { return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil } return nil, errors.New("unexpected host") }) definition := probeDefinition(TypeHTTP) definition.Target = Target{Scheme: "http", Host: "service.internal", Port: port, Path: "/start"} definition.FollowRedirects = false withoutRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition) if err != nil { t.Fatal(err) } if withoutRedirect.State != "up" || withoutRedirect.StatusCode == nil || *withoutRedirect.StatusCode != http.StatusFound { t.Fatalf("redirect was not safely stopped: %+v", withoutRedirect) } definition.FollowRedirects = true withRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition) if err != nil { t.Fatal(err) } if withRedirect.State != "up" || withRedirect.StatusCode == nil || *withRedirect.StatusCode != http.StatusOK { t.Fatalf("redirect was not followed within policy: %+v", withRedirect) } } func TestProbeExecutorTimeoutAndDNS(t *testing.T) { definition := probeDefinition(TypeHTTP) definition.Timeout = time.Second // Use a doer that releases only when the executor's per-probe context expires. blockingDoer := httpDoerFunc(func(request *http.Request) (*http.Response, error) { <-request.Context().Done() return nil, request.Context().Err() }) result, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: blockingDoer}).Execute(context.Background(), definition) if !errors.Is(err, context.DeadlineExceeded) || result.ErrorClass != "transport_error" { t.Fatalf("unexpected timeout result=%+v err=%v", result, err) } dnsDefinition := probeDefinition(TypeDNS) dnsDefinition.Target = Target{Host: "dns.internal"} dnsResolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) { if host != "dns.internal" { return nil, errors.New("unexpected host") } return []netip.Addr{netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("10.0.0.2")}, nil }) dnsResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: dnsResolver}).Execute(context.Background(), dnsDefinition) if err != nil || dnsResult.State != "up" || dnsResult.Attributes["addressCount"] != 2 { t.Fatalf("unexpected DNS result=%+v err=%v", dnsResult, err) } } func TestProbeExecutorTCPAndUnsupportedICMP(t *testing.T) { definition := probeDefinition(TypeTCP) definition.Target = Target{Host: "tcp.internal", Port: 443} calledNetwork := "" tcpResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { calledNetwork = network client, peer := net.Pipe() _ = peer.Close() return client, nil }}).Execute(context.Background(), definition) if err != nil || tcpResult.State != "up" || calledNetwork != "tcp" { t.Fatalf("unexpected TCP result=%+v err=%v network=%s", tcpResult, err, calledNetwork) } icmpDefinition := probeDefinition(TypeICMP) icmpDefinition.Target = Target{Host: "gateway.internal"} icmpResult, err := (ProbeExecutor{Policy: executorPolicy()}).Execute(context.Background(), icmpDefinition) if err != nil || icmpResult.State != "unknown" || icmpResult.ErrorClass != "unsupported" { t.Fatalf("unexpected ICMP fallback result=%+v err=%v", icmpResult, err) } } func TestProbeExecutorTLSCertificateFacts(t *testing.T) { observed := time.Date(2026, time.August, 2, 12, 0, 0, 0, time.UTC) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } certificateTemplate := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "tls.internal"}, DNSNames: []string{"tls.internal"}, NotBefore: observed.Add(-time.Hour), NotAfter: observed.Add(90 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}} certificateDER, err := x509.CreateCertificate(rand.Reader, certificateTemplate, certificateTemplate, &privateKey.PublicKey, privateKey) if err != nil { t.Fatal(err) } serverCertificate := tls.Certificate{Certificate: [][]byte{certificateDER}, PrivateKey: privateKey} definition := probeDefinition(TypeTLS) definition.Target = Target{Scheme: "https", Host: "tls.internal", Port: 443} definition.VerifyTLS = false tlsResult, err := (ProbeExecutor{ Policy: executorPolicy(), Resolver: resolverFunc(func(context.Context, string) ([]netip.Addr, error) { return []netip.Addr{netip.MustParseAddr("10.0.0.3")}, nil }), Now: func() time.Time { return observed }, DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { if network != "tcp" { return nil, errors.New("unexpected TLS network") } client, server := net.Pipe() go func() { defer server.Close() _ = tls.Server(server, &tls.Config{Certificates: []tls.Certificate{serverCertificate}}).Handshake() }() return client, nil }, }).Execute(context.Background(), definition) if err != nil || tlsResult.State != "up" || tlsResult.Certificate == nil || tlsResult.Certificate.VerificationState != "valid" || tlsResult.Certificate.HostnameValid == nil || !*tlsResult.Certificate.HostnameValid { t.Fatalf("unexpected TLS result=%+v err=%v", tlsResult, err) } attention := *certificateTemplate attention.NotAfter = observed.Add(7 * 24 * time.Hour) attentionResult := certificateFromX509(&attention, definition, func() time.Time { return observed }) if attentionResult.VerificationState != "attention" { t.Fatalf("expected certificate attention, got %+v", attentionResult) } invalid := *certificateTemplate invalid.DNSNames = []string{"other.internal"} invalidResult := certificateFromX509(&invalid, definition, func() time.Time { return observed }) if invalidResult.VerificationState != "invalid" || invalidResult.HostnameValid == nil || *invalidResult.HostnameValid { t.Fatalf("expected invalid hostname, got %+v", invalidResult) } } type httpDoerFunc func(*http.Request) (*http.Response, error) func (f httpDoerFunc) Do(request *http.Request) (*http.Response, error) { return f(request) }