iop/apps/edge/internal/openai/openai_auth_routes_models_test.go
toki 8c5b01c893 refactor: repository readable restructuring - OpenAI tests, roadmap, and cleanup
- Add unit tests for OpenAI edge adapter (chat handler, stream reasoning, tool synthesis, auth routes, test support)
- Add roadmap: agent-readable-repository-refactor milestone with subtask plans
- Update PHASE.md, priority-queue.md, .gitignore
- Delete old edge/node/openai.test files (replaced by proper test structure)
2026-07-16 13:38:19 +09:00

169 lines
5.7 KiB
Go

package openai
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
)
func TestRoutesRequireBearerTokenWhenConfigured(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
BearerToken: "secret-token",
Models: []string{"model-a"},
}, &fakeRunService{}, nil)
for _, tc := range []struct {
name string
auth string
wantStatus int
}{
{name: "missing", wantStatus: http.StatusUnauthorized},
{name: "wrong", auth: "Bearer wrong", wantStatus: http.StatusUnauthorized},
{name: "valid", auth: "Bearer secret-token", wantStatus: http.StatusOK},
} {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
if tc.auth != "" {
req.Header.Set("Authorization", tc.auth)
}
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != tc.wantStatus {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
})
}
}
func TestHealthzDoesNotRequireBearerToken(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
}
func TestModelsUsesConfiguredModelsOrTarget(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Target: "fallback-model"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "fallback-model") {
t.Fatalf("expected target model, got %s", w.Body.String())
}
srv = NewServer(config.EdgeOpenAIConf{Models: []string{"a", "b"}}, &fakeRunService{}, nil)
w = httptest.NewRecorder()
srv.handleModels(w, req)
if !strings.Contains(w.Body.String(), `"id":"a"`) || !strings.Contains(w.Body.String(), `"id":"b"`) {
t.Fatalf("expected configured models, got %s", w.Body.String())
}
}
// TestOpenAIModelsUsesRefreshedCatalog verifies that a provider-pool catalog
// applied via SetModelCatalog (config refresh apply) is reflected in /v1/models
// on the next request, replacing the previous catalog snapshot.
func TestOpenAIModelsUsesRefreshedCatalog(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-old"}})
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
if body := w.Body.String(); !strings.Contains(body, `"id":"model-old"`) {
t.Fatalf("expected initial catalog model-old, got %s", body)
}
// Refresh the catalog: the next request must show the new model and not the old.
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-new"}})
w = httptest.NewRecorder()
srv.handleModels(w, req)
body := w.Body.String()
if !strings.Contains(body, `"id":"model-new"`) {
t.Fatalf("expected refreshed catalog model-new, got %s", body)
}
if strings.Contains(body, `"id":"model-old"`) {
t.Fatalf("stale catalog model-old still present after refresh, got %s", body)
}
}
// TestOpenAIProviderPoolRouteUsesRefreshedCatalog verifies that provider-pool
// route resolution reads the refreshed catalog snapshot, so a model added by a
// config refresh is routed via the provider pool.
func TestOpenAIProviderPoolRouteUsesRefreshedCatalog(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
if srv.findProviderPoolEntry("model-new") != nil {
t.Fatal("expected no provider-pool entry before catalog is set")
}
srv.SetModelCatalog([]config.ModelCatalogEntry{{
ID: "model-new",
Providers: map[string]string{"prov-a": "served-a"},
}})
dispatch, ok := srv.resolveRouteDispatch("model-new")
if !ok || !dispatch.ProviderPool {
t.Fatalf("expected provider-pool dispatch for refreshed model, got ok=%v dispatch=%+v", ok, dispatch)
}
}
func TestOllamaAPIPassthrough(t *testing.T) {
fake := &fakeRunService{
ollamaResp: edgeservice.OllamaAPIView{
StatusCode: http.StatusAccepted,
ContentType: "application/json",
Body: `{"ok":true}`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", TimeoutSec: 15}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(`{"model":"gemma4:26b"}`))
w := httptest.NewRecorder()
srv.handleOllamaAPI(w, req)
if w.Code != http.StatusAccepted {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.ollamaReq.Adapter != "ollama" || fake.ollamaReq.Method != http.MethodPost || fake.ollamaReq.Path != "/api/show" {
t.Fatalf("passthrough req mismatch: %+v", fake.ollamaReq)
}
if fake.ollamaReq.Body != `{"model":"gemma4:26b"}` || fake.ollamaReq.TimeoutSec != 15 {
t.Fatalf("passthrough body/timeout mismatch: %+v", fake.ollamaReq)
}
if w.Body.String() != `{"ok":true}` {
t.Fatalf("body: got %s", w.Body.String())
}
}
func TestOllamaAPIPassthroughPreservesConfiguredTarget(t *testing.T) {
fake := &fakeRunService{
ollamaResp: edgeservice.OllamaAPIView{
StatusCode: http.StatusOK,
ContentType: "application/json",
Body: `{"models":[]}`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b", TimeoutSec: 15}, fake, nil)
req := httptest.NewRequest(http.MethodGet, "/api/tags", nil)
w := httptest.NewRecorder()
srv.handleOllamaAPI(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.ollamaReq.Target != "gemma4:26b" {
t.Fatalf("passthrough target: got %q, want gemma4:26b", fake.ollamaReq.Target)
}
}