test: 添加各模块单元测试(config/llm/service/handler)
This commit is contained in:
322
backend/internal/handler/handler_test.go
Normal file
322
backend/internal/handler/handler_test.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"ai-agent-scaffold-go/internal/model"
|
||||
"ai-agent-scaffold-go/internal/service"
|
||||
"ai-agent-scaffold-go/pkg/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// Stub Runner for handler tests
|
||||
// ============================================================
|
||||
|
||||
type stubRunner struct {
|
||||
sessionID string
|
||||
runResult []string
|
||||
runErr error
|
||||
}
|
||||
|
||||
func (r *stubRunner) CreateSession(userID string) (string, error) {
|
||||
if r.sessionID != "" {
|
||||
return r.sessionID, nil
|
||||
}
|
||||
return "sess:" + userID + ":1", nil
|
||||
}
|
||||
|
||||
func (r *stubRunner) Run(userID, sessionID string, content model.ChatContent) ([]string, error) {
|
||||
return r.runResult, r.runErr
|
||||
}
|
||||
|
||||
func (r *stubRunner) Stream(userID, sessionID string, content model.ChatContent) (<-chan string, <-chan error) {
|
||||
outputs := make(chan string, 4)
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(outputs)
|
||||
defer close(errs)
|
||||
if r.runErr != nil {
|
||||
errs <- r.runErr
|
||||
return
|
||||
}
|
||||
for _, s := range r.runResult {
|
||||
outputs <- s
|
||||
}
|
||||
}()
|
||||
return outputs, errs
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
func setupRouter() (*gin.Engine, *model.InMemoryAgentRegistry) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
registry := model.NewInMemoryAgentRegistry()
|
||||
sessions := model.NewInMemorySessionStore()
|
||||
svc := service.NewChatService(registry, sessions)
|
||||
router := gin.New()
|
||||
RegisterRoutes(router, svc)
|
||||
return router, registry
|
||||
}
|
||||
|
||||
func doRequest(router http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != "" {
|
||||
req = httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func parseEnvelope(t *testing.T, w *httptest.ResponseRecorder) Envelope {
|
||||
t.Helper()
|
||||
var resp Envelope
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// healthz 测试
|
||||
// ============================================================
|
||||
|
||||
func TestHealthz_Returns200(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/healthz", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
w := doRequest(router, "GET", "/healthz", "")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// queryAgentConfigList 测试
|
||||
// ============================================================
|
||||
|
||||
func TestQueryAgentConfigList_ReturnsList(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{AgentID: "1", AgentName: "a", AgentDesc: "desc a"})
|
||||
registry.Register(model.RegisteredAgent{AgentID: "2", AgentName: "b", AgentDesc: "desc b"})
|
||||
|
||||
w := doRequest(router, "GET", "/api/v1/query_ai_agent_config_list", "")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
|
||||
data, _ := json.Marshal(resp.Data)
|
||||
var agents []AiAgentConfigResponse
|
||||
json.Unmarshal(data, &agents)
|
||||
assert.Len(t, agents, 2)
|
||||
}
|
||||
|
||||
func TestQueryAgentConfigList_Empty_ReturnsEmptyArray(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
w := doRequest(router, "GET", "/api/v1/query_ai_agent_config_list", "")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// createSession 测试
|
||||
// ============================================================
|
||||
|
||||
func TestCreateSession_Success(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{
|
||||
AgentID: "1",
|
||||
Runner: &stubRunner{sessionID: "sess:1:1"},
|
||||
})
|
||||
|
||||
body := `{"agentId":"1","userId":"user1"}`
|
||||
w := doRequest(router, "POST", "/api/v1/create_session", body)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
|
||||
data, _ := json.Marshal(resp.Data)
|
||||
var sessResp CreateSessionResponse
|
||||
json.Unmarshal(data, &sessResp)
|
||||
assert.Equal(t, "sess:1:1", sessResp.SessionID)
|
||||
}
|
||||
|
||||
func TestCreateSession_AgentNotFound_Returns0003(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
body := `{"agentId":"nonexistent","userId":"user1"}`
|
||||
w := doRequest(router, "POST", "/api/v1/create_session", body)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeAgentNotFound, resp.Code)
|
||||
}
|
||||
|
||||
func TestCreateSession_MissingParams_Returns0002(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
// 空 body
|
||||
w := doRequest(router, "POST", "/api/v1/create_session", "")
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeIllegalParameter, resp.Code)
|
||||
}
|
||||
|
||||
func TestCreateSession_Query_Success(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{
|
||||
AgentID: "1",
|
||||
Runner: &stubRunner{sessionID: "sess:u1:1"},
|
||||
})
|
||||
|
||||
w := doRequest(router, "GET", "/api/v1/create_session?agentId=1&userId=user1", "")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
}
|
||||
|
||||
func TestCreateSession_Query_AgentNotFound_Returns0003(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
w := doRequest(router, "GET", "/api/v1/create_session?agentId=x&userId=u", "")
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeAgentNotFound, resp.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// chat 测试
|
||||
// ============================================================
|
||||
|
||||
func TestChat_Success(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{
|
||||
AgentID: "1",
|
||||
Runner: &stubRunner{sessionID: "s1", runResult: []string{"hello"}},
|
||||
})
|
||||
|
||||
body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}`
|
||||
w := doRequest(router, "POST", "/api/v1/chat", body)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
|
||||
data, _ := json.Marshal(resp.Data)
|
||||
var chatResp ChatResponse
|
||||
json.Unmarshal(data, &chatResp)
|
||||
assert.Equal(t, "hello", chatResp.Content)
|
||||
}
|
||||
|
||||
func TestChat_AgentNotFound_Returns0003(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
body := `{"agentId":"nonexistent","userId":"u1","message":"hi"}`
|
||||
w := doRequest(router, "POST", "/api/v1/chat", body)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeAgentNotFound, resp.Code)
|
||||
}
|
||||
|
||||
func TestChat_MissingBody_Returns0002(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
w := doRequest(router, "POST", "/api/v1/chat", "")
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeIllegalParameter, resp.Code)
|
||||
}
|
||||
|
||||
func TestChat_RunnerError_ReturnsUnknownError(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{
|
||||
AgentID: "1",
|
||||
Runner: &stubRunner{sessionID: "s1", runErr: assert.AnError},
|
||||
})
|
||||
|
||||
body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}`
|
||||
w := doRequest(router, "POST", "/api/v1/chat", body)
|
||||
|
||||
resp := parseEnvelope(t, w)
|
||||
assert.Equal(t, types.CodeUnknownError, resp.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// chatStream 测试
|
||||
// ============================================================
|
||||
|
||||
func TestChatStream_SSEHeaders(t *testing.T) {
|
||||
router, registry := setupRouter()
|
||||
registry.Register(model.RegisteredAgent{
|
||||
AgentID: "1",
|
||||
Runner: &stubRunner{sessionID: "s1", runResult: []string{"chunk1", "chunk2"}},
|
||||
})
|
||||
|
||||
body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}`
|
||||
w := doRequest(router, "POST", "/api/v1/chat_stream", body)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Contains(t, w.Header().Get("Content-Type"), "text/event-stream")
|
||||
assert.Equal(t, "no-cache", w.Header().Get("Cache-Control"))
|
||||
}
|
||||
|
||||
func TestChatStream_AgentNotFound_ReturnsError(t *testing.T) {
|
||||
router, _ := setupRouter()
|
||||
|
||||
body := `{"agentId":"nonexistent","userId":"u1","message":"hi"}`
|
||||
w := doRequest(router, "POST", "/api/v1/chat_stream", body)
|
||||
|
||||
// SSE 流式中错误通过 event 发送,HTTP 状态码仍为 200
|
||||
assert.Equal(t, 200, w.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// writeError 测试
|
||||
// ============================================================
|
||||
|
||||
func TestWriteError_AppError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
writeError(c, types.NewAppError(types.CodeAgentNotFound, "not found"))
|
||||
|
||||
var resp Envelope
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, types.CodeAgentNotFound, resp.Code)
|
||||
}
|
||||
|
||||
func TestWriteError_UnknownError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
writeError(c, assert.AnError)
|
||||
|
||||
var resp Envelope
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, types.CodeUnknownError, resp.Code)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// success 测试
|
||||
// ============================================================
|
||||
|
||||
func TestSuccess_ReturnsEnvelope(t *testing.T) {
|
||||
resp := success(map[string]string{"key": "val"})
|
||||
assert.Equal(t, types.CodeSuccess, resp.Code)
|
||||
assert.Equal(t, types.InfoSuccess, resp.Info)
|
||||
assert.NotNil(t, resp.Data)
|
||||
}
|
||||
Reference in New Issue
Block a user