214 lines
5.8 KiB
Go
214 lines
5.8 KiB
Go
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)
|
||
}
|
||
})
|
||
}
|