package main import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" ) func TestSetupStaticFiles(t *testing.T) { gin.SetMode(gin.TestMode) distFS, err := frontendDistFS() if err != nil { t.Skipf("跳过测试: 前端资源未构建: %v", err) return } r := gin.New() setupStaticFilesWithFS(r, distFS) t.Run("API 404", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/test", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 404 { t.Errorf("期望状态码 404, 实际 %d", w.Code) } }) t.Run("OpenAI proxy prefix 404", func(t *testing.T) { req := httptest.NewRequest("GET", "/openai/", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("期望状态码 404, 实际 %d", w.Code) } if !strings.Contains(w.Body.String(), "not found") { t.Errorf("期望返回 API 风格错误,实际 %s", w.Body.String()) } }) t.Run("Anthropic proxy prefix 404", func(t *testing.T) { req := httptest.NewRequest("GET", "/anthropic/", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("期望状态码 404, 实际 %d", w.Code) } if !strings.Contains(w.Body.String(), "not found") { t.Errorf("期望返回 API 风格错误,实际 %s", w.Body.String()) } }) t.Run("SPA fallback", func(t *testing.T) { req := httptest.NewRequest("GET", "/providers", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Errorf("期望状态码 200, 实际 %d", w.Code) } }) t.Run("MIME type for JS", func(t *testing.T) { req := httptest.NewRequest("GET", "/assets/test.js", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code == 200 { expected := "application/javascript" if w.Header().Get("Content-Type") != expected { t.Errorf("期望 Content-Type %s, 实际 %s", expected, w.Header().Get("Content-Type")) } } else { t.Log("文件不存在,跳过 MIME 类型验证") } }) t.Run("MIME type for CSS", func(t *testing.T) { req := httptest.NewRequest("GET", "/assets/test.css", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code == 200 { expected := "text/css" if w.Header().Get("Content-Type") != expected { t.Errorf("期望 Content-Type %s, 实际 %s", expected, w.Header().Get("Content-Type")) } } else { t.Log("文件不存在,跳过 MIME 类型验证") } }) t.Log("静态文件服务测试通过") } func TestWithProtocolAndStaticRoutes(t *testing.T) { gin.SetMode(gin.TestMode) distFS, err := frontendDistFS() if err != nil { t.Skipf("跳过测试: 前端资源未构建: %v", err) return } r := gin.New() var gotProtocol string var gotPath string r.Any("/openai/*path", withProtocol("openai", func(c *gin.Context) { gotProtocol = c.Param("protocol") gotPath = c.Param("path") c.JSON(http.StatusOK, gin.H{"protocol": gotProtocol, "path": gotPath}) })) r.Any("/anthropic/*path", withProtocol("anthropic", func(c *gin.Context) { gotProtocol = c.Param("protocol") gotPath = c.Param("path") c.JSON(http.StatusOK, gin.H{"protocol": gotProtocol, "path": gotPath}) })) setupStaticFilesWithFS(r, distFS) t.Run("OpenAI route enters proxy handler wrapper", func(t *testing.T) { gotProtocol = "" gotPath = "" req := httptest.NewRequest("POST", "/openai/v1/chat/completions", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("期望状态码 200, 实际 %d", w.Code) } if gotProtocol != "openai" { t.Errorf("期望 protocol=openai, 实际 %s", gotProtocol) } if gotPath != "/v1/chat/completions" { t.Errorf("期望 path=/v1/chat/completions, 实际 %s", gotPath) } }) t.Run("Anthropic route enters proxy handler wrapper", func(t *testing.T) { gotProtocol = "" gotPath = "" req := httptest.NewRequest("POST", "/anthropic/v1/messages", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("期望状态码 200, 实际 %d", w.Code) } if gotProtocol != "anthropic" { t.Errorf("期望 protocol=anthropic, 实际 %s", gotProtocol) } if gotPath != "/v1/messages" { t.Errorf("期望 path=/v1/messages, 实际 %s", gotPath) } }) t.Run("Static assets are not hijacked", func(t *testing.T) { gotProtocol = "" gotPath = "" req := httptest.NewRequest("GET", "/assets/test.js", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if gotProtocol != "" || gotPath != "" { t.Errorf("静态资源不应进入代理包装器,实际 protocol=%s path=%s", gotProtocol, gotPath) } if w.Code == http.StatusOK { if !strings.HasPrefix(w.Header().Get("Content-Type"), "application/javascript") { t.Errorf("期望 JS Content-Type, 实际 %s", w.Header().Get("Content-Type")) } return } if w.Code != http.StatusNotFound { t.Errorf("期望静态资源返回 200 或 404, 实际 %d", w.Code) } }) t.Run("SPA path keeps fallback", func(t *testing.T) { req := httptest.NewRequest("GET", "/providers", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("期望状态码 200, 实际 %d", w.Code) } if !strings.Contains(w.Header().Get("Content-Type"), "text/html") { t.Errorf("期望返回 HTML,实际 %s", w.Header().Get("Content-Type")) } }) t.Run("Unknown proxy-like path does not return index html", func(t *testing.T) { req := httptest.NewRequest("GET", "/openai/unknown", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("显式代理路由应进入代理包装器,实际状态码 %d", w.Code) } if gotProtocol != "openai" || gotPath != "/unknown" { t.Errorf("期望 unknown 代理路径进入 openai 包装器,实际 protocol=%s path=%s", gotProtocol, gotPath) } }) }