package reporter import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "mfd-monitor-agent/internal/status" ) // StaticProvider 是用于测试的固定缓存快照提供者。 type StaticProvider struct { Snapshot status.HostStatus OK bool } // Get 返回配置的静态快照与就绪标志。 func (p StaticProvider) Get() (status.HostStatus, bool) { return p.Snapshot, p.OK } // TestReportPostsHostStatusJSON 验证上报复用 HostStatus JSON 载荷。 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 验证非 2xx 响应导致单次上报失败。 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 验证循环对失败上报重试一次。 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 验证重试等待可被 ctx 取消。 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()) } }