Pull the private `client`/`postJSON` from `internal/ai` into a standalone `internal/httpclient` package so both the AI providers and the upcoming webhook senders can share the same bounded-reader, timeout-guarded JSON HTTP client without introducing a dependency cycle. Changes: - internal/httpclient: Client struct with PostJSON(ctx, url, headers, payload, out), functional options WithTimeout/WithTransport, a 4 MiB response cap, and a 60s default timeout - internal/ai: three providers (OpenAI, Ollama, Anthropic) now embed an `*httpclient.Client` (field renamed from `client` to `http`); the old `client.go` is deleted - All 11 test packages pass (ai tests are byte-for-byte unaffected) This zero-behaviour refactor unblocks the webhook package distributed in M5, which needs the exact same JSON-post-and-decode helper.
79 lines
2.0 KiB
Go
79 lines
2.0 KiB
Go
package httpclient
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPostJSON(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Error("Content-Type not set")
|
|
}
|
|
var in map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, _ := in["key"].(string); v != "value" {
|
|
t.Errorf(`key = %q, want "value"`, v)
|
|
}
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
var out struct {
|
|
OK bool `json:"ok"`
|
|
}
|
|
c := New()
|
|
if err := c.PostJSON(t.Context(), srv.URL, nil, map[string]string{"key": "value"}, &out); err != nil {
|
|
t.Fatalf("PostJSON: %v", err)
|
|
}
|
|
if !out.OK {
|
|
t.Error("OK = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestPostJSONHTTPError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New()
|
|
if err := c.PostJSON(t.Context(), srv.URL, nil, nil, nil); err == nil {
|
|
t.Error("PostJSON(500) succeeded, want error")
|
|
}
|
|
}
|
|
|
|
func TestPostJSONCustomHeaders(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-Custom"); got != "abc" {
|
|
t.Errorf("X-Custom = %q, want abc", got)
|
|
}
|
|
_, _ = w.Write([]byte(`{"x":1}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
var out map[string]int
|
|
c := New()
|
|
if err := c.PostJSON(t.Context(), srv.URL, map[string]string{"X-Custom": "abc"}, nil, &out); err != nil {
|
|
t.Fatalf("PostJSON: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWithTimeout(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(WithTimeout(10 * time.Millisecond))
|
|
if err := c.PostJSON(t.Context(), srv.URL, nil, nil, nil); err == nil {
|
|
t.Error("PostJSON with short timeout succeeded, want error")
|
|
}
|
|
}
|