323 lines
9.2 KiB
Go
323 lines
9.2 KiB
Go
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)
|
||
}
|