Files
Pulse/internal/tempproxy/client_coverage_test.go
2025-12-29 17:25:21 +00:00

489 lines
15 KiB
Go

package tempproxy
import (
"context"
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
type writeErrConn struct {
net.Conn
}
func (c writeErrConn) Write(p []byte) (int, error) {
return 0, errors.New("write failed")
}
func startUnixServer(t *testing.T, handler func(net.Conn)) (string, func()) {
t.Helper()
dir := t.TempDir()
socketPath := filepath.Join(dir, "proxy.sock")
ln, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatalf("listen: %v", err)
}
done := make(chan struct{})
go func() {
defer close(done)
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handler(conn)
}
}()
cleanup := func() {
ln.Close()
<-done
}
return socketPath, cleanup
}
func startRPCServer(t *testing.T, handler func(RPCRequest) RPCResponse) (string, func()) {
t.Helper()
return startUnixServer(t, func(conn net.Conn) {
defer conn.Close()
decoder := json.NewDecoder(conn)
var req RPCRequest
if err := decoder.Decode(&req); err != nil {
return
}
resp := handler(req)
encoder := json.NewEncoder(conn)
_ = encoder.Encode(resp)
})
}
func TestNewClientSocketSelection(t *testing.T) {
origStat := statFn
t.Cleanup(func() { statFn = origStat })
t.Run("EnvOverride", func(t *testing.T) {
origEnv := os.Getenv("PULSE_SENSOR_PROXY_SOCKET")
t.Cleanup(func() {
if origEnv == "" {
os.Unsetenv("PULSE_SENSOR_PROXY_SOCKET")
} else {
os.Setenv("PULSE_SENSOR_PROXY_SOCKET", origEnv)
}
})
os.Setenv("PULSE_SENSOR_PROXY_SOCKET", "/tmp/custom.sock")
statFn = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist }
client := NewClient()
if client.socketPath != "/tmp/custom.sock" {
t.Fatalf("expected env socket, got %q", client.socketPath)
}
})
t.Run("DefaultExists", func(t *testing.T) {
statFn = func(path string) (os.FileInfo, error) {
if path == defaultSocketPath {
return nil, nil
}
return nil, os.ErrNotExist
}
client := NewClient()
if client.socketPath != defaultSocketPath {
t.Fatalf("expected default socket, got %q", client.socketPath)
}
})
t.Run("ContainerExists", func(t *testing.T) {
statFn = func(path string) (os.FileInfo, error) {
if path == containerSocketPath {
return nil, nil
}
return nil, os.ErrNotExist
}
client := NewClient()
if client.socketPath != containerSocketPath {
t.Fatalf("expected container socket, got %q", client.socketPath)
}
})
t.Run("FallbackDefault", func(t *testing.T) {
statFn = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist }
client := NewClient()
if client.socketPath != defaultSocketPath {
t.Fatalf("expected fallback socket, got %q", client.socketPath)
}
})
}
func TestClientIsAvailable(t *testing.T) {
origStat := statFn
t.Cleanup(func() { statFn = origStat })
client := &Client{socketPath: "/tmp/socket"}
statFn = func(string) (os.FileInfo, error) { return nil, nil }
if !client.IsAvailable() {
t.Fatalf("expected available")
}
statFn = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist }
if client.IsAvailable() {
t.Fatalf("expected unavailable")
}
}
func TestCallOnceSuccessAndDeadline(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
if req.Method != "ping" {
t.Fatalf("unexpected method: %s", req.Method)
}
return RPCResponse{Success: true}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
resp, err := client.callOnce(context.Background(), "ping", nil)
if err != nil || resp == nil || !resp.Success {
t.Fatalf("expected success: resp=%v err=%v", resp, err)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
resp, err = client.callOnce(ctx, "ping", nil)
if err != nil || resp == nil || !resp.Success {
t.Fatalf("expected success with deadline: resp=%v err=%v", resp, err)
}
}
func TestCallOnceErrors(t *testing.T) {
t.Run("ConnectError", func(t *testing.T) {
client := &Client{socketPath: "/tmp/missing.sock", timeout: 10 * time.Millisecond}
if _, err := client.callOnce(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected connect error")
}
})
t.Run("EncodeError", func(t *testing.T) {
socketPath, cleanup := startUnixServer(t, func(conn net.Conn) {
conn.Close()
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.callOnce(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected encode error")
}
})
t.Run("EncodeErrorDial", func(t *testing.T) {
origDial := dialContextFn
t.Cleanup(func() { dialContextFn = origDial })
clientConn, serverConn := net.Pipe()
t.Cleanup(func() { _ = serverConn.Close() })
dialContextFn = func(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
return writeErrConn{Conn: clientConn}, nil
}
client := &Client{socketPath: "/tmp/unused.sock", timeout: 50 * time.Millisecond}
if _, err := client.callOnce(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected encode error")
}
})
t.Run("DecodeError", func(t *testing.T) {
socketPath, cleanup := startUnixServer(t, func(conn net.Conn) {
defer conn.Close()
_, _ = conn.Write([]byte("{"))
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.callOnce(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected decode error")
}
})
}
func TestCallWithContextBehavior(t *testing.T) {
t.Run("Success", func(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
resp, err := client.callWithContext(context.Background(), "ping", nil)
if err != nil || resp == nil || !resp.Success {
t.Fatalf("expected success: resp=%v err=%v", resp, err)
}
})
t.Run("NonRetryable", func(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "unauthorized"}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.callWithContext(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected auth error")
}
})
t.Run("CancelledBeforeRetry", func(t *testing.T) {
client := &Client{socketPath: "/tmp/missing.sock", timeout: 10 * time.Millisecond}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := client.callWithContext(ctx, "ping", nil); err == nil {
t.Fatalf("expected cancelled error")
}
})
t.Run("CancelledDuringBackoff", func(t *testing.T) {
client := &Client{socketPath: "/tmp/missing.sock", timeout: 10 * time.Millisecond}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
if _, err := client.callWithContext(ctx, "ping", nil); err == nil {
t.Fatalf("expected backoff cancel error")
}
})
t.Run("MaxRetriesExhausted", func(t *testing.T) {
client := &Client{socketPath: "/tmp/missing.sock", timeout: 10 * time.Millisecond}
if _, err := client.callWithContext(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected retry error")
}
})
}
func TestGetStatus(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{"ok": true}}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
data, err := client.GetStatus()
if err != nil || data["ok"] != true {
t.Fatalf("unexpected status: %v err=%v", data, err)
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "boom"}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.GetStatus(); err == nil {
t.Fatalf("expected status error")
}
}
func TestRegisterNodes(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{
"nodes": []interface{}{map[string]interface{}{"name": "node1"}},
}}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
nodes, err := client.RegisterNodes()
if err != nil || len(nodes) != 1 || nodes[0]["name"] != "node1" {
t.Fatalf("unexpected nodes: %v err=%v", nodes, err)
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "unauthorized"}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.RegisterNodes(); err == nil {
t.Fatalf("expected proxy error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{}}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.RegisterNodes(); err == nil {
t.Fatalf("expected missing nodes error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{"nodes": "bad"}}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.RegisterNodes(); err == nil {
t.Fatalf("expected nodes type error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{"nodes": []interface{}{"bad"}}}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.RegisterNodes(); err == nil {
t.Fatalf("expected node map error")
}
}
func TestGetTemperature(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{"temperature": "42"}}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
temp, err := client.GetTemperature("node1")
if err != nil || temp != "42" {
t.Fatalf("unexpected temp: %q err=%v", temp, err)
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "unauthorized"}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.GetTemperature("node1"); err == nil {
t.Fatalf("expected proxy error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "boom"}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.GetTemperature("node1"); err == nil {
t.Fatalf("expected unknown error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{}}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.GetTemperature("node1"); err == nil {
t.Fatalf("expected missing temperature error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true, Data: map[string]interface{}{"temperature": 12}}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.GetTemperature("node1"); err == nil {
t.Fatalf("expected type error")
}
}
func TestRequestCleanup(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
if req.Method != "request_cleanup" {
t.Fatalf("unexpected method: %s", req.Method)
}
if req.Params["host"] != "node1" {
t.Fatalf("unexpected params: %v", req.Params)
}
return RPCResponse{Success: true}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if err := client.RequestCleanup("node1"); err != nil {
t.Fatalf("unexpected cleanup error: %v", err)
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "boom"}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if err := client.RequestCleanup(""); err == nil {
t.Fatalf("expected proxy error")
}
socketPath, cleanup = startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: ""}
})
t.Cleanup(cleanup)
client = &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if err := client.RequestCleanup(""); err == nil {
t.Fatalf("expected rejected error")
}
client = &Client{socketPath: "/tmp/missing.sock", timeout: 10 * time.Millisecond}
if err := client.RequestCleanup(""); err == nil {
t.Fatalf("expected call error")
}
}
func TestCallWrapper(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: true}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
resp, err := client.call("ping", nil)
if err != nil || resp == nil || !resp.Success {
t.Fatalf("expected call success: resp=%v err=%v", resp, err)
}
}
func TestCallWithContextRetryableResponseError(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: "ssh timeout"}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.callWithContext(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected retry exhaustion")
}
}
func TestCallWithContextSuccessOnRetry(t *testing.T) {
count := 0
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
count++
if count == 1 {
return RPCResponse{Success: false, Error: "ssh timeout"}
}
return RPCResponse{Success: true}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
resp, err := client.callWithContext(context.Background(), "ping", nil)
if err != nil || resp == nil || !resp.Success {
t.Fatalf("expected retry success: resp=%v err=%v", resp, err)
}
}
func TestCallWithContextRespErrorString(t *testing.T) {
socketPath, cleanup := startRPCServer(t, func(req RPCRequest) RPCResponse {
return RPCResponse{Success: false, Error: strings.Repeat("x", 1)}
})
t.Cleanup(cleanup)
client := &Client{socketPath: socketPath, timeout: 50 * time.Millisecond}
if _, err := client.callWithContext(context.Background(), "ping", nil); err == nil {
t.Fatalf("expected error")
}
}