实现统一模型 ID 格式 (provider_id/model_name),支持跨协议模型标识和 Smart Passthrough。 核心变更: - 新增 pkg/modelid 包:解析、格式化、校验统一模型 ID - 数据库迁移:models 表使用 UUID 主键 + UNIQUE(provider_id, model_name) 约束 - Repository 层:FindByProviderAndModelName、ListEnabled 方法 - Service 层:联合唯一校验、provider ID 字符集校验 - Conversion 层:ExtractModelName、RewriteRequestModelName/RewriteResponseModelName 方法 - Handler 层:统一模型 ID 路由、Smart Passthrough、Models API 本地聚合 - 新增 error-responses、unified-model-id 规范 测试覆盖: - 单元测试:modelid、conversion、handler、service、repository - 集成测试:统一模型 ID 路由、Smart Passthrough 保真性、跨协议转换 - 迁移测试:UUID 主键、UNIQUE 约束、级联删除 OpenSpec: - 归档 unified-model-id 变更到 archive/2026-04-21-unified-model-id - 同步 11 个 delta specs 到 main specs - 新增 error-responses、unified-model-id 规范文件
165 lines
4.7 KiB
Go
165 lines
4.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"nex/backend/internal/domain"
|
|
)
|
|
|
|
func TestProviderHandler_CreateProvider_Success(t *testing.T) {
|
|
h := NewProviderHandler(&mockProviderService{})
|
|
|
|
body, _ := json.Marshal(map[string]string{
|
|
"id": "p1",
|
|
"name": "Test",
|
|
"api_key": "sk-test",
|
|
"base_url": "https://api.test.com",
|
|
})
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("POST", "/api/providers", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateProvider(c)
|
|
assert.Equal(t, 201, w.Code)
|
|
|
|
var result domain.Provider
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result))
|
|
assert.Equal(t, "p1", result.ID)
|
|
assert.Contains(t, result.APIKey, "***")
|
|
}
|
|
|
|
func TestProviderHandler_CreateProvider_WithProtocol(t *testing.T) {
|
|
h := NewProviderHandler(&mockProviderService{})
|
|
|
|
body, _ := json.Marshal(map[string]string{
|
|
"id": "p1",
|
|
"name": "Test",
|
|
"api_key": "sk-test",
|
|
"base_url": "https://api.test.com",
|
|
"protocol": "anthropic",
|
|
})
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("POST", "/api/providers", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateProvider(c)
|
|
assert.Equal(t, 201, w.Code)
|
|
}
|
|
|
|
func TestProviderHandler_UpdateProvider(t *testing.T) {
|
|
h := NewProviderHandler(&mockProviderService{
|
|
provider: &domain.Provider{ID: "p1", Name: "Updated", APIKey: "***"},
|
|
})
|
|
|
|
body, _ := json.Marshal(map[string]string{"name": "Updated"})
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "p1"}}
|
|
c.Request = httptest.NewRequest("PUT", "/api/providers/p1", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.UpdateProvider(c)
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
func TestProviderHandler_UpdateProvider_InvalidBody(t *testing.T) {
|
|
h := NewProviderHandler(&mockProviderService{})
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "p1"}}
|
|
c.Request = httptest.NewRequest("PUT", "/api/providers/p1", nil)
|
|
|
|
h.UpdateProvider(c)
|
|
assert.Equal(t, 400, w.Code)
|
|
}
|
|
|
|
func TestProviderHandler_DeleteProvider(t *testing.T) {
|
|
h := NewProviderHandler(&mockProviderService{})
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "p1"}}
|
|
c.Request = httptest.NewRequest("DELETE", "/api/providers/p1", bytes.NewReader([]byte{}))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.DeleteProvider(c)
|
|
assert.True(t, w.Code == 204 || w.Code == 200)
|
|
}
|
|
|
|
func TestModelHandler_DeleteModel(t *testing.T) {
|
|
h := NewModelHandler(&mockModelService{})
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "m1"}}
|
|
c.Request = httptest.NewRequest("DELETE", "/api/models/m1", bytes.NewReader([]byte{}))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.DeleteModel(c)
|
|
assert.True(t, w.Code == 204 || w.Code == 200)
|
|
}
|
|
|
|
func TestModelHandler_CreateModel_Success(t *testing.T) {
|
|
h := NewModelHandler(&mockModelService{})
|
|
|
|
body, _ := json.Marshal(map[string]string{
|
|
"provider_id": "p1",
|
|
"model_name": "gpt-4",
|
|
})
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("POST", "/api/models", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateModel(c)
|
|
assert.Equal(t, 201, w.Code)
|
|
|
|
var result domain.Model
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result))
|
|
assert.NotEmpty(t, result.ID)
|
|
}
|
|
|
|
func TestModelHandler_GetModel(t *testing.T) {
|
|
h := NewModelHandler(&mockModelService{
|
|
model: &domain.Model{ID: "m1", ModelName: "gpt-4"},
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "m1"}}
|
|
c.Request = httptest.NewRequest("GET", "/api/models/m1", nil)
|
|
|
|
h.GetModel(c)
|
|
assert.Equal(t, 200, w.Code)
|
|
|
|
var result domain.Model
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result))
|
|
assert.Equal(t, "gpt-4", result.ModelName)
|
|
}
|
|
|
|
func TestModelHandler_UpdateModel(t *testing.T) {
|
|
h := NewModelHandler(&mockModelService{
|
|
model: &domain.Model{ID: "m1", ModelName: "gpt-4o"},
|
|
})
|
|
|
|
body, _ := json.Marshal(map[string]string{"model_name": "gpt-4o"})
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "m1"}}
|
|
c.Request = httptest.NewRequest("PUT", "/api/models/m1", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.UpdateModel(c)
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|