337 lines
11 KiB
Go
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{}
|
|
} |