package client import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/monlet/agent/internal/scheduler" ) func TestSendHeartbeatSuccess(t *testing.T) { var got HeartbeatRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/heartbeat" || r.Header.Get("Authorization") != "Bearer tok" { http.Error(w, "bad", http.StatusUnauthorized) return } _ = json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusAccepted) _, _ = io.WriteString(w, `{"accepted":true}`) })) defer srv.Close() c := New(srv.URL, "tok", "a1") err := c.SendHeartbeat(context.Background(), HeartbeatRequest{ AgentID: "a1", ObservedAt: time.Now().UTC(), Hostname: "h", Features: AgentFeatures{Push: true, Metrics: false}, }) if err != nil { t.Fatal(err) } if got.AgentID != "a1" { t.Fatalf("got %+v", got) } } func TestSendEventsBatch(t *testing.T) { var got EventBatchRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusAccepted) _, _ = io.WriteString(w, `{"accepted":2,"deduplicated":0}`) })) defer srv.Close() c := New(srv.URL, "tok", "a1") resp, err := c.SendEvents(context.Background(), []scheduler.Event{{EventID: "x"}, {EventID: "y"}}) if err != nil { t.Fatal(err) } if resp.Accepted != 2 || got.AgentID != "a1" || len(got.Events) != 2 { t.Fatalf("resp=%+v got=%+v", resp, got) } } func TestSendEventsSplitsByJSONSize(t *testing.T) { var ( requests int total int ) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Error(err) http.Error(w, "read body", http.StatusInternalServerError) return } if len(body) > MaxBatchBytes { t.Errorf("body size %d exceeds %d", len(body), MaxBatchBytes) http.Error(w, "too large", http.StatusRequestEntityTooLarge) return } var got EventBatchRequest if err := json.Unmarshal(body, &got); err != nil { t.Error(err) http.Error(w, "bad json", http.StatusBadRequest) return } requests++ total += len(got.Events) w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(EventBatchResponse{Accepted: len(got.Events)}) })) defer srv.Close() events := make([]scheduler.Event, MaxBatchEvents) for i := range events { events[i] = scheduler.Event{ EventID: "event-" + time.Unix(int64(i), 0).UTC().Format("20060102150405"), CheckID: "check", ObservedAt: time.Unix(int64(i), 0).UTC(), Status: "ok", Output: strings.Repeat("x", 8192), } } c := New(srv.URL, "tok", "a1") resp, err := c.SendEvents(context.Background(), events) if err != nil { t.Fatal(err) } if requests < 2 { t.Fatalf("expected split batch, got %d request", requests) } if total != MaxBatchEvents || resp.Accepted != MaxBatchEvents { t.Fatalf("total=%d resp=%+v", total, resp) } } func TestSendEventsTooBig(t *testing.T) { c := New("http://x", "t", "a1") big := make([]scheduler.Event, MaxBatchEvents+1) _, err := c.SendEvents(context.Background(), big) if err == nil { t.Fatal("expected error") } if Retryable(err) { t.Fatal("oversized batch should not retry") } } func TestSendEventsSingleEventTooLargeIsNotRetryable(t *testing.T) { c := New("http://x", "t", "a1") _, err := c.SendEvents(context.Background(), []scheduler.Event{{ EventID: "x", CheckID: strings.Repeat("c", MaxBatchBytes), Status: "ok", }}) if err == nil { t.Fatal("expected error") } if Retryable(err) { t.Fatal("oversized single event should not retry") } } func TestRetryableClassification(t *testing.T) { if Retryable(&HTTPError{Status: 400, Body: ""}) { t.Fatal("4xx should not retry") } if !Retryable(&HTTPError{Status: 503}) { t.Fatal("5xx should retry") } if !Retryable(context.DeadlineExceeded) { t.Fatal("network error should retry") } } func TestBackoffBounds(t *testing.T) { for a := 1; a <= 10; a++ { d := Delay(a) if d < BackoffMin*4/5 { t.Fatalf("attempt %d: delay %s below min", a, d) } if d > BackoffMax*6/5 { t.Fatalf("attempt %d: delay %s above max", a, d) } } }