package devtodev import ( "bytes" "compress/gzip" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/klauspost/compress/zstd" ) func TestSend_NoCompression(t *testing.T) { payload := samplePayload() expected, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal payload: %v", err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("method: %s", r.Method) } if ct := r.Header.Get("Content-Type"); ct != "application/json" { t.Fatalf("content-type: %s", ct) } if r.URL.Query().Get("appId") != "app-123" { t.Fatalf("appId: %s", r.URL.Query().Get("appId")) } got, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read body: %v", err) } if !bytes.Equal(got, expected) { t.Fatalf("body mismatch") } w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewClient("app-123") client.Endpoint = server.URL if err := client.Send(context.Background(), payload); err != nil { t.Fatalf("send: %v", err) } } func TestSend_GzipCompression(t *testing.T) { payload := samplePayload() expected, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal payload: %v", err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ct := r.Header.Get("Content-Type"); ct != "application/gzip" { t.Fatalf("content-type: %s", ct) } zr, err := gzip.NewReader(r.Body) if err != nil { t.Fatalf("gzip reader: %v", err) } got, err := io.ReadAll(zr) if err != nil { t.Fatalf("read body: %v", err) } zr.Close() if !bytes.Equal(got, expected) { t.Fatalf("body mismatch") } w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewClient("app-123") client.Endpoint = server.URL client.Compression = CompressionGzip if err := client.Send(context.Background(), payload); err != nil { t.Fatalf("send: %v", err) } } func TestSend_ZstdCompression(t *testing.T) { payload := samplePayload() expected, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal payload: %v", err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ct := r.Header.Get("Content-Type"); ct != "application/zstd" { t.Fatalf("content-type: %s", ct) } decoder, err := zstd.NewReader(r.Body) if err != nil { t.Fatalf("zstd reader: %v", err) } got, err := io.ReadAll(decoder) if err != nil { t.Fatalf("read body: %v", err) } decoder.Close() if !bytes.Equal(got, expected) { t.Fatalf("body mismatch") } w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewClient("app-123") client.Endpoint = server.URL client.Compression = CompressionZstd if err := client.Send(context.Background(), payload); err != nil { t.Fatalf("send: %v", err) } } func TestValidatePayload(t *testing.T) { cases := []struct { name string payload Payload ok bool }{ { name: "empty", payload: Payload{}, ok: false, }, { name: "missing device id", payload: Payload{ Reports: []Report{{}}, }, ok: false, }, { name: "missing packages", payload: Payload{ Reports: []Report{{DeviceID: "d1"}}, }, ok: false, }, { name: "missing events", payload: Payload{ Reports: []Report{{DeviceID: "d1", Packages: []Package{{}}}}, }, ok: false, }, { name: "missing code", payload: Payload{ Reports: []Report{{DeviceID: "d1", Packages: []Package{{Events: []Event{{"timestamp": int64(1)}}}}}}, }, ok: false, }, { name: "missing timestamp", payload: Payload{ Reports: []Report{{DeviceID: "d1", Packages: []Package{{Events: []Event{{"code": "ce"}}}}}}, }, ok: false, }, { name: "ok", payload: Payload{ Reports: []Report{{DeviceID: "d1", Packages: []Package{{Events: []Event{{"code": "ce", "timestamp": int64(1)}}}}}}, }, ok: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := ValidatePayload(tc.payload) if tc.ok && err != nil { t.Fatalf("expected ok, got %v", err) } if !tc.ok && err == nil { t.Fatalf("expected error") } }) } } func TestSendWithResponse_ParsesJSONError(t *testing.T) { payload := samplePayload() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message":"bad request"}`)) })) defer server.Close() client := NewClient("app-123") client.Endpoint = server.URL client.Retry.MaxAttempts = 1 resp, err := client.SendWithResponse(context.Background(), payload) if err == nil { t.Fatalf("expected error") } if resp == nil || resp.StatusCode != http.StatusBadRequest { t.Fatalf("unexpected response: %+v", resp) } var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("expected APIError, got %T", err) } if apiErr.Message != "bad request" { t.Fatalf("unexpected message: %q", apiErr.Message) } } func samplePayload() Payload { return Payload{ Reports: []Report{ { DeviceID: "device-123", Packages: []Package{ { Language: "en", Country: "US", Events: []Event{ { "code": "ce", "timestamp": time.Now().UnixMilli(), "level": 5, "name": "custom_event", "parameters": map[string]interface{}{ "level": 5, }, }, }, }, }, }, }, } }