package reporter import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "mfd-monitor-agent/internal/status" ) // StaticProvider is a test provider with a fixed cached snapshot. type StaticProvider struct { Snapshot status.HostStatus OK bool } // Get returns the configured static snapshot and readiness flag. func (p StaticProvider) Get() (status.HostStatus, bool) { return p.Snapshot, p.OK } // TestReportPostsHostStatusJSON verifies reports reuse the HostStatus JSON payload. func TestReportPostsHostStatusJSON(t *testing.T) { var got status.HostStatus server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("method = %s", r.Method) } if r.Header.Get("Content-Type") != "application/json" { t.Fatalf("content type = %q", r.Header.Get("Content-Type")) } if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Fatal(err) } w.WriteHeader(http.StatusAccepted) })) defer server.Close() client := NewClient(server.URL, time.Second) err := client.Report(status.HostStatus{Host: status.HostInfo{Hostname: "host-a"}}) if err != nil { t.Fatal(err) } if got.Host.Hostname != "host-a" { t.Fatalf("posted hostname = %q", got.Host.Hostname) } } // TestReportReturnsErrorForHTTPFailure verifies non-2xx responses fail one report. func TestReportReturnsErrorForHTTPFailure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() client := NewClient(server.URL, time.Second) if err := client.Report(status.HostStatus{}); err == nil { t.Fatal("expected error for HTTP 500") } } // TestRunRetriesFailedReportOnce verifies the loop retries one failed report. func TestRunRetriesFailedReportOnce(t *testing.T) { var requests atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if requests.Add(1) == 1 { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) })) defer server.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() provider := StaticProvider{Snapshot: status.HostStatus{Host: status.HostInfo{Hostname: "host-a"}}, OK: true} client := NewClient(server.URL, time.Second) done := make(chan struct{}) go func() { defer close(done) runWithRetryDelay(ctx, provider, client, 10*time.Millisecond, time.Millisecond, slog.New(slog.NewTextHandler(io.Discard, nil))) }() deadline := time.After(time.Second) for requests.Load() < 2 { select { case <-deadline: t.Fatalf("requests = %d", requests.Load()) case <-time.After(time.Millisecond): } } cancel() <-done } // TestRunStopsDuringRetryDelayWhenContextCanceled verifies retry waits honor cancellation. func TestRunStopsDuringRetryDelayWhenContextCanceled(t *testing.T) { var requests atomic.Int32 firstRequest := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if requests.Add(1) == 1 { close(firstRequest) } w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() ctx, cancel := context.WithCancel(context.Background()) provider := StaticProvider{Snapshot: status.HostStatus{}, OK: true} client := NewClient(server.URL, time.Second) done := make(chan struct{}) go func() { defer close(done) runWithRetryDelay(ctx, provider, client, time.Millisecond, time.Hour, slog.New(slog.NewTextHandler(io.Discard, nil))) }() select { case <-firstRequest: case <-time.After(time.Second): t.Fatal("initial report did not run") } cancel() select { case <-done: case <-time.After(100 * time.Millisecond): t.Fatal("run did not stop during retry delay") } if requests.Load() != 1 { t.Fatalf("requests = %d", requests.Load()) } }