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{} }