1575 lines
42 KiB
Go
1575 lines
42 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"codeberg.org/towerops-agent/towerops-agent/pb"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ExecuteCheck routing tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExecuteCheck_UnknownCheckType(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-1",
|
|
CheckType: "foobar",
|
|
TimeoutMs: 5000,
|
|
})
|
|
if result.Status != 3 {
|
|
t.Fatalf("expected status 3, got %d", result.Status)
|
|
}
|
|
if result.CheckId != "chk-1" {
|
|
t.Fatalf("expected CheckId chk-1, got %s", result.CheckId)
|
|
}
|
|
if !strings.Contains(result.Output, "Unknown check type") {
|
|
t.Fatalf("expected unknown check type message, got %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_MissingHTTPConfig(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-2",
|
|
CheckType: "http",
|
|
TimeoutMs: 5000,
|
|
// No Config set
|
|
})
|
|
if result.Status != 3 {
|
|
t.Fatalf("expected status 3, got %d", result.Status)
|
|
}
|
|
if !strings.Contains(result.Output, "Missing HTTP config") {
|
|
t.Fatalf("expected missing HTTP config message, got %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_MissingTCPConfig(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-3",
|
|
CheckType: "tcp",
|
|
TimeoutMs: 5000,
|
|
})
|
|
if result.Status != 3 {
|
|
t.Fatalf("expected status 3, got %d", result.Status)
|
|
}
|
|
if !strings.Contains(result.Output, "Missing TCP config") {
|
|
t.Fatalf("expected missing TCP config message, got %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_MissingDNSConfig(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-4",
|
|
CheckType: "dns",
|
|
TimeoutMs: 5000,
|
|
})
|
|
if result.Status != 3 {
|
|
t.Fatalf("expected status 3, got %d", result.Status)
|
|
}
|
|
if !strings.Contains(result.Output, "Missing DNS config") {
|
|
t.Fatalf("expected missing DNS config message, got %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_SetsCheckIdAndTimestamp(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-ts",
|
|
CheckType: "unknown",
|
|
TimeoutMs: 5000,
|
|
})
|
|
if result.CheckId != "chk-ts" {
|
|
t.Fatalf("expected CheckId chk-ts, got %s", result.CheckId)
|
|
}
|
|
if result.Timestamp == 0 {
|
|
t.Fatal("expected non-zero timestamp")
|
|
}
|
|
if result.ResponseTimeMs < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", result.ResponseTimeMs)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_HTTPRouting(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-route-http",
|
|
CheckType: "http",
|
|
TimeoutMs: 5000,
|
|
Config: &pb.Check_Http{
|
|
Http: &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
},
|
|
},
|
|
})
|
|
if result.Status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", result.Status, result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_TCPRouting(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
_, portStr, _ := net.SplitHostPort(ln.Addr().String())
|
|
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-route-tcp",
|
|
CheckType: "tcp",
|
|
TimeoutMs: 5000,
|
|
Config: &pb.Check_Tcp{
|
|
Tcp: &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
},
|
|
},
|
|
})
|
|
if result.Status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", result.Status, result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_DNSRouting(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-route-dns",
|
|
CheckType: "dns",
|
|
TimeoutMs: 5000,
|
|
Config: &pb.Check_Dns{
|
|
Dns: &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "A",
|
|
},
|
|
},
|
|
})
|
|
// DNS for localhost may or may not resolve depending on system config,
|
|
// but the routing should work regardless.
|
|
if result.Status == 3 {
|
|
t.Fatalf("expected routing to DNS handler, got status 3 (UNKNOWN): %s", result.Output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HTTP check tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHTTPCheck_SuccessfulGET(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "OK")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, rt := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "HTTP 200 OK") {
|
|
t.Fatalf("expected HTTP 200 OK in output, got %s", output)
|
|
}
|
|
if rt < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_CustomMethod_POST(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
w.WriteHeader(405)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "post",
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_CustomExpectedStatus(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(201)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
ExpectedStatus: 201,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_WrongStatusCode(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(500)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "500") && !strings.Contains(output, "expected 200") {
|
|
t.Fatalf("expected status code mismatch message, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_DefaultMethodIsGET(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("expected default GET, got %s", r.Method)
|
|
w.WriteHeader(405)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "", // should default to GET
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_DefaultExpectedStatusIs200(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
ExpectedStatus: 0, // should default to 200
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_VerifySslFalseWithSelfSigned(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// With VerifySsl=false (InsecureSkipVerify=true), the self-signed cert should be accepted
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
VerifySsl: false,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with VerifySsl=false, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_VerifySslTrueRejectsSelfSigned(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// With VerifySsl=true (InsecureSkipVerify=false), self-signed cert should fail
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
VerifySsl: true,
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 with VerifySsl=true on self-signed, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Request failed") {
|
|
t.Fatalf("expected request failed message, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_FollowRedirectsTrue(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/redirect" {
|
|
http.Redirect(w, r, "/final", http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "final page")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL + "/redirect",
|
|
FollowRedirects: true,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 after following redirect, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "200") {
|
|
t.Fatalf("expected final 200 status in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_FollowRedirectsFalse(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/redirect" {
|
|
http.Redirect(w, r, "/final", http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// When FollowRedirects is false, we should see the 301 directly
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL + "/redirect",
|
|
FollowRedirects: false,
|
|
ExpectedStatus: 301,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 (matching 301), got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_FollowRedirectsFalse_DefaultExpects200(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/final", http.StatusMovedPermanently)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// FollowRedirects=false and default expected=200, but we get 301 → CRITICAL
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL + "/redirect",
|
|
FollowRedirects: false,
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 (301 != 200), got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_RegexMatchSucceeds(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "Hello World! Version 1.2.3")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Regex: `Version \d+\.\d+\.\d+`,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for regex match, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_RegexMatchFails(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "Hello World!")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Regex: `Version \d+`,
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for regex mismatch, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "does not match") {
|
|
t.Fatalf("expected 'does not match' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_InvalidRegex(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "some body")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Regex: `[invalid`,
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for invalid regex, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Invalid regex") {
|
|
t.Fatalf("expected 'Invalid regex' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_CustomHeaders(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("X-Custom") != "test-value" {
|
|
t.Errorf("expected X-Custom header to be test-value, got %s", r.Header.Get("X-Custom"))
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer abc123" {
|
|
t.Errorf("expected Authorization header, got %s", r.Header.Get("Authorization"))
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Headers: map[string]string{
|
|
"X-Custom": "test-value",
|
|
"Authorization": "Bearer abc123",
|
|
},
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_UnreachableServer(t *testing.T) {
|
|
// Use a non-routable address to guarantee failure
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: "http://192.0.2.1:1/unreachable",
|
|
}, 1000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Request failed") {
|
|
t.Fatalf("expected 'Request failed' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_InvalidURL(t *testing.T) {
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: "://not-a-url",
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Failed to create request") {
|
|
t.Fatalf("expected 'Failed to create request' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_RequestWithBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
buf := make([]byte, 1024)
|
|
n, _ := r.Body.Read(buf)
|
|
body := string(buf[:n])
|
|
if body != `{"key":"value"}` {
|
|
t.Errorf("expected JSON body, got %s", body)
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "POST",
|
|
Body: `{"key":"value"}`,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_ContextCancellation(t *testing.T) {
|
|
// Server that hangs forever
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
// Cancel after a short delay
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
status, output, _ := executeHTTPCheck(ctx, &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 30000) // long timeout so the context cancel hits first
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 on context cancel, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Request failed") {
|
|
t.Fatalf("expected 'Request failed' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_LargeResponseBodyWithRegex(t *testing.T) {
|
|
// Generate a large body (500KB) with a marker at the end
|
|
bigBody := strings.Repeat("a", 500*1024) + "MARKER_FOUND_HERE"
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, bigBody)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Regex: `MARKER_FOUND_HERE`,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for regex match in large body, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_SlowServerTimeout(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(500 * time.Millisecond)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 50) // 50ms timeout, server takes 500ms
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for timeout, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_EmptyBody_NoRegex(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TCP check tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestTCPCheck_PortOpen(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
// Accept connections in background so dial doesn't hang
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, rt := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, fmt.Sprintf("TCP port %d open", port)) {
|
|
t.Fatalf("expected port open message, got %s", output)
|
|
}
|
|
if rt < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_PortClosed(t *testing.T) {
|
|
// Find a port that's definitely not listening by binding and immediately closing
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := parsePort(portFromListener(ln))
|
|
_ = ln.Close() // close immediately so port is refused
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
}, 2000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Connection failed") {
|
|
t.Fatalf("expected 'Connection failed' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_SendExpectSuccess(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
// Echo server
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() { _ = c.Close() }()
|
|
scanner := bufio.NewScanner(c)
|
|
if scanner.Scan() {
|
|
line := scanner.Text()
|
|
_, _ = fmt.Fprintf(c, "ECHO:%s\n", line)
|
|
}
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: "hello\n",
|
|
Expect: "ECHO:hello",
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_SendExpectMismatch(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() { _ = c.Close() }()
|
|
buf := make([]byte, 1024)
|
|
_, _ = c.Read(buf)
|
|
_, _ = fmt.Fprint(c, "WRONG_RESPONSE")
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: "hello",
|
|
Expect: "EXPECTED_VALUE",
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Unexpected response") {
|
|
t.Fatalf("expected 'Unexpected response' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_SendWithEmptyExpect(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() { _ = c.Close() }()
|
|
buf := make([]byte, 1024)
|
|
_, _ = c.Read(buf)
|
|
// Don't send anything back
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: "data\n",
|
|
Expect: "", // no expect check
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 (send only, no expect), got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_IPv6Localhost(t *testing.T) {
|
|
ln, err := net.Listen("tcp6", "[::1]:0")
|
|
if err != nil {
|
|
t.Skip("IPv6 not available on this system")
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "::1",
|
|
Port: port,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for IPv6, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_ReadTimeoutOnExpect(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
// Server that accepts and reads but never writes back
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() { _ = c.Close() }()
|
|
buf := make([]byte, 1024)
|
|
_, _ = c.Read(buf)
|
|
// Intentionally never respond - hold connection open
|
|
time.Sleep(10 * time.Second)
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: "hello",
|
|
Expect: "response",
|
|
}, 200) // short timeout
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for read timeout, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Receive failed") {
|
|
t.Fatalf("expected 'Receive failed' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_VeryShortTimeout(t *testing.T) {
|
|
// Use TEST-NET address that won't respond
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "192.0.2.1",
|
|
Port: 80,
|
|
}, 1) // 1ms timeout - should fail
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_BinaryDataSendExpect(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
// Server that echoes binary data back
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() { _ = c.Close() }()
|
|
buf := make([]byte, 4096)
|
|
n, err := c.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, _ = c.Write(buf[:n])
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
// Send some binary-ish data
|
|
sendData := "BIN\x00\x01\x02DATA"
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: sendData,
|
|
Expect: sendData,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for binary echo, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DNS check tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestDNSCheck_ARecord(t *testing.T) {
|
|
status, output, rt := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "A",
|
|
}, 5000)
|
|
|
|
// localhost should resolve on most systems, but skip if it doesn't
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available for localhost")
|
|
}
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Resolved to:") {
|
|
t.Fatalf("expected 'Resolved to:' in output, got %s", output)
|
|
}
|
|
if rt < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_AAAARecord(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "AAAA",
|
|
}, 5000)
|
|
|
|
// AAAA for localhost may or may not exist - just verify it doesn't crash
|
|
// and returns a valid status
|
|
if status != 0 && status != 2 {
|
|
t.Fatalf("expected status 0 or 2, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_CNAMERecord(t *testing.T) {
|
|
// Use a well-known domain that has a CNAME
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "www.google.com",
|
|
RecordType: "CNAME",
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available")
|
|
}
|
|
// CNAME lookup may return the hostname itself if no CNAME exists
|
|
if status != 0 && status != 2 {
|
|
t.Fatalf("expected status 0 or 2, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_MXRecord(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "google.com",
|
|
RecordType: "MX",
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available")
|
|
}
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for MX lookup, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Resolved to:") {
|
|
t.Fatalf("expected 'Resolved to:' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_TXTRecord(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "google.com",
|
|
RecordType: "TXT",
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available")
|
|
}
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 for TXT lookup, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_UnsupportedRecordType(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "example.com",
|
|
RecordType: "SRV",
|
|
}, 5000)
|
|
|
|
if status != 3 {
|
|
t.Fatalf("expected status 3, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Unsupported record type") {
|
|
t.Fatalf("expected 'Unsupported record type' in output, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_ExpectedMatchesResult(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "A",
|
|
Expected: "127.0.0.1",
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available for localhost")
|
|
}
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_ExpectedDoesNotMatch(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "A",
|
|
Expected: "10.99.99.99",
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available for localhost")
|
|
}
|
|
// If localhost resolved, the expected won't match
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for mismatched expected, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Expected '10.99.99.99'") {
|
|
t.Fatalf("expected mismatch message, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_NonexistentDomain(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "this-domain-does-not-exist-towerops-test.invalid",
|
|
RecordType: "A",
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for NXDOMAIN, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_DefaultRecordTypeIsA(t *testing.T) {
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "", // should default to A
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available for localhost")
|
|
}
|
|
// Should behave the same as explicitly specifying "A"
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with default record type, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_CustomDNSServer(t *testing.T) {
|
|
// Test with Google's public DNS
|
|
conn, err := net.DialTimeout("udp", "8.8.8.8:53", 2*time.Second)
|
|
if err != nil {
|
|
t.Skip("Cannot reach 8.8.8.8:53, skipping custom DNS server test")
|
|
}
|
|
_ = conn.Close()
|
|
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "example.com",
|
|
RecordType: "A",
|
|
Server: "8.8.8.8",
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with custom DNS server, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_RecordTypeCaseInsensitive(t *testing.T) {
|
|
// The code does strings.ToUpper, so lowercase should work
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "localhost",
|
|
RecordType: "a", // lowercase
|
|
}, 5000)
|
|
|
|
if status == 2 && strings.Contains(output, "DNS query failed") {
|
|
t.Skip("DNS resolution not available for localhost")
|
|
}
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with lowercase record type, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_NoRecordsFound(t *testing.T) {
|
|
// AAAA for a domain that likely only has A records
|
|
// Use a known domain that almost certainly won't have AAAA
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "this-domain-does-not-exist-towerops-test.invalid",
|
|
RecordType: "AAAA",
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestDNSCheck_VeryShortTimeout(t *testing.T) {
|
|
// Use a custom DNS server with 1ms timeout - should timeout
|
|
status, output, _ := executeDNSCheck(context.Background(), &pb.DnsCheckConfig{
|
|
Hostname: "example.com",
|
|
RecordType: "A",
|
|
Server: "8.8.8.8",
|
|
}, 1) // 1ms timeout
|
|
|
|
// Should fail due to timeout
|
|
if status != 2 {
|
|
// On very fast networks this could actually succeed, so just verify it doesn't crash
|
|
t.Logf("DNS with 1ms timeout returned status %d: %s (may succeed on fast networks)", status, output)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_SendFailsOnClosedConnection(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
// Server accepts and immediately closes the connection
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
|
|
// Send a large payload to trigger a write error on a closed connection.
|
|
// The first small write might succeed (kernel buffer), but a large write
|
|
// after the peer has closed should fail with a broken pipe or similar.
|
|
largePayload := strings.Repeat("x", 1024*1024) // 1MB
|
|
// Give the server time to close the connection
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
status, output, _ := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
Send: largePayload,
|
|
Expect: "something",
|
|
}, 5000)
|
|
|
|
// This may hit either "Send failed" or "Receive failed" depending on timing
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 for write to closed conn, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Edge case tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHTTPCheck_MethodUppercased(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "PUT" {
|
|
t.Errorf("expected PUT, got %s", r.Method)
|
|
w.WriteHeader(405)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "put", // lowercase
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_ResponseTimeIsPositive(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(10 * time.Millisecond)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
_, _, rt := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
}, 5000)
|
|
|
|
if rt <= 0 {
|
|
t.Fatalf("expected positive response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestTCPCheck_ResponseTimeReported(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := parsePort(portFromListener(ln))
|
|
_, _, rt := executeTCPCheck(context.Background(), &pb.TcpCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
}, 5000)
|
|
|
|
if rt < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_ResponseTimeFallback(t *testing.T) {
|
|
// When status/output are set directly (unknown type), responseTimeMs should be calculated
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-fallback",
|
|
CheckType: "invalid",
|
|
TimeoutMs: 5000,
|
|
})
|
|
|
|
if result.ResponseTimeMs < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", result.ResponseTimeMs)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_TLSServerNoVerify(t *testing.T) {
|
|
// Create a TLS server with custom cert
|
|
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "secure")
|
|
}))
|
|
srv.TLS = &tls.Config{}
|
|
srv.StartTLS()
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
VerifySsl: false,
|
|
Regex: "secure",
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_HeadMethod(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "HEAD" {
|
|
t.Errorf("expected HEAD, got %s", r.Method)
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "HEAD",
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_DeleteMethod(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "DELETE" {
|
|
t.Errorf("expected DELETE, got %s", r.Method)
|
|
}
|
|
w.WriteHeader(204)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Method: "DELETE",
|
|
ExpectedStatus: 204,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_RegexOnEmptyBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
// No body written
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Regex: "something",
|
|
}, 5000)
|
|
|
|
if status != 2 {
|
|
t.Fatalf("expected status 2 (regex no match on empty body), got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_MultipleRedirects(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/a":
|
|
http.Redirect(w, r, "/b", http.StatusFound)
|
|
case "/b":
|
|
http.Redirect(w, r, "/c", http.StatusFound)
|
|
case "/c":
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprint(w, "final")
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL + "/a",
|
|
FollowRedirects: true,
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 after multiple redirects, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestHTTPCheck_EmptyHeadersMap(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
status, output, _ := executeHTTPCheck(context.Background(), &pb.HttpCheckConfig{
|
|
Url: srv.URL,
|
|
Headers: map[string]string{},
|
|
}, 5000)
|
|
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with empty headers, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SSL check tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExecuteCheck_MissingSSLConfig(t *testing.T) {
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-ssl-missing",
|
|
CheckType: "ssl",
|
|
TimeoutMs: 5000,
|
|
})
|
|
if result.Status != 3 {
|
|
t.Fatalf("expected status 3, got %d", result.Status)
|
|
}
|
|
if !strings.Contains(result.Output, "Missing SSL config") {
|
|
t.Fatalf("expected missing SSL config message, got %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCheck_SSLRouting(t *testing.T) {
|
|
// Start a local TLS server with a self-signed cert
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
withTestSSLRootCA(t, srv.Certificate())
|
|
|
|
// Parse the port from the test server URL
|
|
_, portStr, _ := net.SplitHostPort(strings.TrimPrefix(strings.TrimPrefix(srv.URL, "https://"), "http://"))
|
|
|
|
result := ExecuteCheck(context.Background(), &pb.Check{
|
|
Id: "chk-route-ssl",
|
|
CheckType: "ssl",
|
|
TimeoutMs: 5000,
|
|
Config: &pb.Check_Ssl{
|
|
Ssl: &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
WarningDays: 7,
|
|
},
|
|
},
|
|
})
|
|
// The test server has a valid cert, so should be OK
|
|
if result.Status == 3 {
|
|
t.Fatalf("expected routing to SSL handler, got status 3 (UNKNOWN): %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_ValidCert(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
withTestSSLRootCA(t, srv.Certificate())
|
|
|
|
_, portStr, _ := net.SplitHostPort(strings.TrimPrefix(srv.URL, "https://"))
|
|
|
|
status, output, rt := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
WarningDays: 1,
|
|
}, 5000)
|
|
|
|
// httptest TLS cert is valid, should be OK with warning_days=1
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "OK: Certificate") {
|
|
t.Fatalf("expected OK message, got %s", output)
|
|
}
|
|
if rt < 0 {
|
|
t.Fatalf("expected non-negative response time, got %f", rt)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_WarningThreshold(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
withTestSSLRootCA(t, srv.Certificate())
|
|
|
|
_, portStr, _ := net.SplitHostPort(strings.TrimPrefix(srv.URL, "https://"))
|
|
|
|
// httptest certs typically expire within a few years.
|
|
// Set warning_days very high to trigger WARNING.
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
WarningDays: 999999,
|
|
}, 5000)
|
|
|
|
if status != 1 {
|
|
t.Fatalf("expected status 1 (WARNING), got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "WARNING: Certificate") {
|
|
t.Fatalf("expected WARNING message, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_ConnectionFailure(t *testing.T) {
|
|
// Use non-routable address
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "192.0.2.1",
|
|
Port: 443,
|
|
}, 1000)
|
|
|
|
if status != 3 {
|
|
t.Fatalf("expected status 3 (UNKNOWN), got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Connection failed") {
|
|
t.Fatalf("expected connection failed message, got %s", output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_ClosedPort(t *testing.T) {
|
|
// Bind and immediately close to get a port that refuses connections
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := parsePort(portFromListener(ln))
|
|
_ = ln.Close()
|
|
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: port,
|
|
}, 2000)
|
|
|
|
if status != 3 {
|
|
t.Fatalf("expected status 3, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_DefaultPort443(t *testing.T) {
|
|
// When port is 0 (unset), should default to 443.
|
|
// We just verify it doesn't crash.
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "192.0.2.1",
|
|
Port: 0,
|
|
}, 500)
|
|
|
|
// Should fail to connect (non-routable), but shouldn't crash
|
|
if status != 3 {
|
|
t.Logf("SSL check with default port returned status %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_DefaultWarningDays30(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
withTestSSLRootCA(t, srv.Certificate())
|
|
|
|
_, portStr, _ := net.SplitHostPort(strings.TrimPrefix(srv.URL, "https://"))
|
|
|
|
// warning_days=0 should default to 30
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
WarningDays: 0,
|
|
}, 5000)
|
|
|
|
// httptest certs are valid for ~1 year, with default 30 days warning should be OK
|
|
if status != 0 {
|
|
t.Fatalf("expected status 0 with default warning_days, got %d: %s", status, output)
|
|
}
|
|
}
|
|
|
|
func TestSSLCheck_UntrustedCertFails(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
_, portStr, _ := net.SplitHostPort(strings.TrimPrefix(srv.URL, "https://"))
|
|
|
|
status, output, _ := executeSSLCheck(context.Background(), &pb.SslCheckConfig{
|
|
Host: "127.0.0.1",
|
|
Port: parsePort(portStr),
|
|
WarningDays: 7,
|
|
}, 5000)
|
|
|
|
if status != 3 {
|
|
t.Fatalf("expected status 3 for untrusted certificate, got %d: %s", status, output)
|
|
}
|
|
if !strings.Contains(output, "Connection failed") {
|
|
t.Fatalf("expected connection failure output, got %s", output)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// portFromListener extracts the port string from a net.Listener's address.
|
|
func portFromListener(ln net.Listener) string {
|
|
_, port, _ := net.SplitHostPort(ln.Addr().String())
|
|
return port
|
|
}
|
|
|
|
// parsePort converts a port string to uint32 for use in proto configs.
|
|
func parsePort(s string) uint32 {
|
|
var port uint32
|
|
_, _ = fmt.Sscanf(s, "%d", &port)
|
|
return port
|
|
}
|
|
|
|
func withTestSSLRootCA(t *testing.T, cert *x509.Certificate) {
|
|
t.Helper()
|
|
orig := sslRootCAs
|
|
sslRootCAs = func() (*x509.CertPool, error) {
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(cert)
|
|
return pool, nil
|
|
}
|
|
t.Cleanup(func() { sslRootCAs = orig })
|
|
}
|