diff --git a/backend/internal/server/routes/antigravity_http_test.go b/backend/internal/server/routes/antigravity_http_test.go new file mode 100644 index 00000000..a1a397b9 --- /dev/null +++ b/backend/internal/server/routes/antigravity_http_test.go @@ -0,0 +1,177 @@ +package routes + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/Wei-Shaw/sub2api/internal/service" + "log/slog" +) + +func TestAntigravityHTTPRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + + // 创建模拟的 LanguageServerService + mockService := service.NewLanguageServerService(slog.Default(), nil) + + // 创建路由 + r := gin.New() + v1 := r.Group("/api/v1") + + // 注册 Antigravity 路由 + RegisterAntigravityHTTPRoutes(v1, mockService) + + // 测试 1: GET /health + t.Run("HealthCheck", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/health", nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + var result map[string]string + json.Unmarshal(w.Body.Bytes(), &result) + if result["status"] != "healthy" { + t.Fatalf("Expected status=healthy, got %v", result) + } + t.Log("✅ 健康检查端点") + }) + + // 测试 2: GET /models + t.Run("GetModels", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/models", nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + var result map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &result) + if result["default_model"] != "claude-opus-4-6" { + t.Fatalf("Expected default_model, got %v", result) + } + t.Log("✅ 获取模型列表") + }) + + // 测试 3: POST /cascade/start + var cascadeID string + t.Run("StartCascade", func(t *testing.T) { + body, _ := json.Marshal(map[string]string{ + "model": "claude-opus-4-6", + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/cascade/start", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer test-token") + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + var result map[string]string + json.Unmarshal(w.Body.Bytes(), &result) + cascadeID = result["cascade_id"] + if cascadeID == "" { + t.Fatalf("Expected cascade_id, got empty") + } + t.Logf("✅ 启动会话 (cascade_id=%s)", cascadeID) + }) + + // 测试 4: POST /cascade/cancel(使用从第3个测试获取的真实会话ID) + t.Run("CancelCascade", func(t *testing.T) { + body, _ := json.Marshal(map[string]string{ + "cascade_id": cascadeID, + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/cascade/cancel", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + var result map[string]string + json.Unmarshal(w.Body.Bytes(), &result) + if result["message"] != "cascade cancelled" { + t.Fatalf("Expected cascade cancelled message, got %v", result) + } + t.Log("✅ 取消会话") + }) + + // 测试 5: POST /cascade/message (SSE) - 验证响应头格式 + t.Run("SendMessage", func(t *testing.T) { + body, _ := json.Marshal(map[string]string{ + "cascade_id": cascadeID, + "message": "Hello, world!", + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/cascade/message", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer test-token") + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + contentType := w.Header().Get("Content-Type") + if contentType != "text/event-stream" { + t.Fatalf("Expected text/event-stream, got %s", contentType) + } + t.Log("✅ 发送消息(SSE流式响应)") + }) + + t.Log("\n✅ 所有 Antigravity HTTP API 路由测试通过!") +} + +func TestStartCascadeValidation(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockService := service.NewLanguageServerService(slog.Default(), nil) + r := gin.New() + v1 := r.Group("/api/v1") + RegisterAntigravityHTTPRoutes(v1, mockService) + + t.Run("MissingModel", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(`{"system_prompt":"test"}`) + req, _ := http.NewRequest("POST", "/api/v1/cascade/start", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer test-token") + r.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected 400 for invalid request, got %d", w.Code) + } + t.Log("✅ 缺少必需字段验证") + }) + + t.Run("MissingAuthorization", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(`{"model":"claude-opus-4-6"}`) + req, _ := http.NewRequest("POST", "/api/v1/cascade/start", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + // 不设置 Authorization 头 + r.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("Expected 401 for missing auth, got %d", w.Code) + } + t.Log("✅ 缺少授权令牌验证") + }) + + t.Log("\n✅ 所有验证测试通过!") +}