Files
2026-05-12 18:49:52 +08:00

337 lines
11 KiB
Go

package integration
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
// newServerClient routes HTTPClient requests to the given httptest server.
func newServerClient(server *httptest.Server) HTTPClient {
return newTestClient(func(r *http.Request) (*http.Response, error) {
var bodyBytes []byte
if r.Body != nil {
bodyBytes, _ = io.ReadAll(r.Body)
r.Body.Close()
}
// Build a fresh request so RequestURI is not carried over.
newURL, _ := url.Parse(server.URL + r.URL.Path)
newReq, err := http.NewRequestWithContext(r.Context(), r.Method, newURL.String(), bytes.NewReader(bodyBytes))
if err != nil {
return nil, err
}
newReq.Header = r.Header.Clone()
return http.DefaultClient.Do(newReq)
})
}
func newTestClient(fn func(*http.Request) (*http.Response, error)) HTTPClient {
return &mockTransport{fn: fn}
}
type mockTransport struct {
fn func(*http.Request) (*http.Response, error)
}
func (m *mockTransport) Do(req *http.Request) (*http.Response, error) {
return m.fn(req)
}
// ─── OpenAI Adapter Tests ─────────────────────────────────────────────────────
func TestOpenAIAdapter_GetModels_Success(t *testing.T) {
var capturedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuth = r.Header.Get("Authorization")
if got, want := r.URL.Path, "/v1/models"; got != want {
t.Errorf("URL path = %q, want %q", got, want)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
io.WriteString(w, `{
"object": "list",
"data": [
{"id": "gpt-4", "object": "model", "context_window": 8192},
{"id": "gpt-3.5-turbo", "object": "model", "context_window": 16385}
]
}`)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
models, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: "sk-test"})
if err != nil {
t.Fatalf("GetModels error = %v", err)
}
if n := len(models); n != 2 {
t.Fatalf("len(models) = %d, want 2", n)
}
if capturedAuth != "Bearer sk-test" {
t.Errorf("Authorization = %q, want Bearer sk-test", capturedAuth)
}
if models[0].ModelID != "gpt-4" || models[0].ContextLength != 8192 {
t.Errorf("models[0] = %+v", models[0])
}
}
func TestOpenAIAdapter_GetModels_EnvVarFallback(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "sk-env-fallback")
var capturedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"object":"list","data":[{"id":"gpt-4o","object":"model","context_window":128000}]}`)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
models, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: ""})
if err != nil {
t.Fatalf("GetModels error = %v", err)
}
if len(models) != 1 || models[0].ModelID != "gpt-4o" {
t.Errorf("models = %v, want [gpt-4o]", models)
}
if capturedAuth != "Bearer sk-env-fallback" {
t.Errorf("Authorization = %q, want Bearer sk-env-fallback", capturedAuth)
}
}
func TestOpenAIAdapter_GetModels_NoAPIKey(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
adapter := NewOpenAIAdapter(http.DefaultClient)
_, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: ""})
if err == nil {
t.Fatal("expected error for missing API key, got nil")
}
}
func TestOpenAIAdapter_GetModels_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{invalid json`)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
_, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: "sk-test"})
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestOpenAIAdapter_GetModels_NetworkError(t *testing.T) {
adapter := NewOpenAIAdapter(newTestClient(func(r *http.Request) (*http.Response, error) {
return nil, errors.New("connection refused")
}))
_, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: "sk-test"})
if err == nil {
t.Fatal("expected error for network failure, got nil")
}
}
func TestOpenAIAdapter_ProbeAccount_SetsHeaders(t *testing.T) {
var capturedAuth, capturedUA, capturedPath string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuth = r.Header.Get("Authorization")
capturedUA = r.Header.Get("User-Agent")
capturedPath = r.URL.Path
w.WriteHeader(http.StatusOK)
io.WriteString(w, `{"object": "list"}`)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
result := adapter.ProbeAccount(context.Background(), SupplierAccount{
AccountID: 1, Platform: "openai",
APIKey: "sk-probe", BaseURL: server.URL,
})
if capturedAuth != "Bearer sk-probe" {
t.Errorf("Authorization = %q, want Bearer sk-probe", capturedAuth)
}
if capturedUA != "supply-intelligence-probe/1.0" {
t.Errorf("User-Agent = %q, want supply-intelligence-probe/1.0", capturedUA)
}
if capturedPath != "/v1/models" {
t.Errorf("path = %q, want /v1/models", capturedPath)
}
if result.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", result.StatusCode)
}
}
func TestOpenAIAdapter_ProbeAccount_TransportError(t *testing.T) {
adapter := NewOpenAIAdapter(newTestClient(func(r *http.Request) (*http.Response, error) {
return nil, errors.New("dns error")
}))
result := adapter.ProbeAccount(context.Background(), SupplierAccount{APIKey: "sk-test"})
if result.TransportError == nil {
t.Error("TransportError: expected set, got nil")
}
}
func TestOpenAIAdapter_ProbeAccount_500(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
result := adapter.ProbeAccount(context.Background(), SupplierAccount{APIKey: "sk-test"})
if result.StatusCode != 500 {
t.Errorf("status = %d, want 500", result.StatusCode)
}
}
func TestOpenAIAdapter_Platform(t *testing.T) {
if got := NewOpenAIAdapter(http.DefaultClient).Platform(); got != "openai" {
t.Errorf("Platform() = %q, want openai", got)
}
}
func TestOpenAIAdapter_HealthCheck_200(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
if err := adapter.HealthCheck(context.Background(), SupplierAccount{APIKey: "sk-test"}); err != nil {
t.Errorf("HealthCheck = %v, want nil", err)
}
}
func TestOpenAIAdapter_HealthCheck_401(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
if err := adapter.HealthCheck(context.Background(), SupplierAccount{APIKey: "sk-test"}); err != nil {
t.Errorf("HealthCheck 401 = %v, want nil (reachable)", err)
}
}
func TestOpenAIAdapter_HealthCheck_503(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
adapter := NewOpenAIAdapter(newServerClient(server))
if err := adapter.HealthCheck(context.Background(), SupplierAccount{APIKey: "sk-test"}); err == nil {
t.Error("HealthCheck 503: expected error, got nil")
}
}
// ─── Anthropic Adapter Tests ─────────────────────────────────────────────────
func TestAnthropicAdapter_GetModels_ReturnsStaticList(t *testing.T) {
adapter := NewAnthropicAdapter(http.DefaultClient)
models, err := adapter.GetModels(context.Background(), SupplierAccount{APIKey: "sk-ant"})
if err != nil {
t.Fatalf("GetModels error = %v", err)
}
wantIDs := []string{
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
}
if len(models) != len(wantIDs) {
t.Fatalf("len(models) = %d, want %d", len(models), len(wantIDs))
}
for i, m := range models {
if m.ModelID != wantIDs[i] {
t.Errorf("models[%d].ModelID = %q, want %q", i, m.ModelID, wantIDs[i])
}
if m.ContextLength == 0 {
t.Errorf("models[%d].ContextLength = 0, want > 0", i)
}
}
}
func TestAnthropicAdapter_ProbeAccount_SetsHeaders(t *testing.T) {
var capturedKey, capturedVersion, capturedPath string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedKey = r.Header.Get("x-api-key")
capturedVersion = r.Header.Get("anthropic-version")
capturedPath = r.URL.Path
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
adapter := NewAnthropicAdapter(newServerClient(server))
result := adapter.ProbeAccount(context.Background(), SupplierAccount{
AccountID: 2, Platform: "anthropic",
APIKey: "sk-ant-probe", BaseURL: server.URL,
})
if capturedKey != "sk-ant-probe" {
t.Errorf("x-api-key = %q, want sk-ant-probe", capturedKey)
}
if capturedVersion != "2023-06-01" {
t.Errorf("anthropic-version = %q, want 2023-06-01", capturedVersion)
}
if capturedPath != "/v1/models" {
t.Errorf("path = %q, want /v1/models", capturedPath)
}
if result.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", result.StatusCode)
}
}
func TestAnthropicAdapter_ProbeAccount_TransportError(t *testing.T) {
adapter := NewAnthropicAdapter(newTestClient(func(r *http.Request) (*http.Response, error) {
return nil, errors.New("connection reset")
}))
result := adapter.ProbeAccount(context.Background(), SupplierAccount{APIKey: "sk-test"})
if result.TransportError == nil {
t.Error("TransportError: expected set, got nil")
}
}
func TestAnthropicAdapter_Platform(t *testing.T) {
if got := NewAnthropicAdapter(http.DefaultClient).Platform(); got != "anthropic" {
t.Errorf("Platform() = %q, want anthropic", got)
}
}
func TestAnthropicAdapter_HealthCheck_200(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
adapter := NewAnthropicAdapter(newServerClient(server))
if err := adapter.HealthCheck(context.Background(), SupplierAccount{APIKey: "sk-ant"}); err != nil {
t.Errorf("HealthCheck = %v, want nil", err)
}
}
func TestAnthropicAdapter_HealthCheck_401(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
adapter := NewAnthropicAdapter(newServerClient(server))
if err := adapter.HealthCheck(context.Background(), SupplierAccount{APIKey: "sk-ant"}); err != nil {
t.Errorf("HealthCheck 401 = %v, want nil (reachable)", err)
}
}
// ─── HTTPClient Interface Compile Check ──────────────────────────────────────
func TestHTTPClientInterface_Implements(t *testing.T) {
var _ HTTPClient = &http.Client{}
var _ HTTPClient = &mockTransport{}
}