gitFlow/internal/httpclient/client_test.go
dimitar b83a4baebd refactor: M1 — extract shared HTTP client from AI package
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.
2026-08-02 09:04:12 +02:00

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")
}
}