go-admin registered its welcome page on GET / in every non-prod mode, so a reverse proxy that forwarded / to the server showed 「GO-ADMIN欢迎您」 instead of the Admin (happened after the 2026-09-28 server migration). When dist/index.html exists, / now returns it; without a dist (vite development) the previous welcome/prod behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
137 lines
4.9 KiB
Go
137 lines
4.9 KiB
Go
package router
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func newSPAEngine(t *testing.T, withDist bool) *gin.Engine {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
dist := filepath.Join(t.TempDir(), "dist")
|
|
if withDist {
|
|
if err := os.MkdirAll(filepath.Join(dist, "assets"), 0o755); err != nil {
|
|
t.Fatalf("建目录失败: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("<!doctype html>SPA"), 0o644); err != nil {
|
|
t.Fatalf("写 index.html 失败: %v", err)
|
|
}
|
|
}
|
|
t.Setenv(SPADirEnv, dist)
|
|
engine := gin.New()
|
|
engine.GET("/api/admin/v1/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") })
|
|
InitSPARouter(engine)
|
|
return engine
|
|
}
|
|
|
|
func do(engine *gin.Engine, method, path string) *httptest.ResponseRecorder {
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, httptest.NewRequest(method, path, nil))
|
|
return recorder
|
|
}
|
|
|
|
// history 模式:前端路由必须回退到 index.html,否则刷新页面就是 404。
|
|
func TestUnmatchedGetFallsBackToIndex(t *testing.T) {
|
|
engine := newSPAEngine(t, true)
|
|
response := do(engine, http.MethodGet, "/goauto/syb-products")
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("前端路由应回退到 index.html,实际 %d", response.Code)
|
|
}
|
|
if body := response.Body.String(); body != "<!doctype html>SPA" {
|
|
t.Fatalf("返回的不是 index.html: %q", body)
|
|
}
|
|
}
|
|
|
|
// `[必须]` 写错的 API 路径必须还是 404。回一个 200 + HTML,客户端看到的会是
|
|
// JSON 解析错误,而不是"这个接口不存在"。
|
|
func TestUnknownAPIPathStaysA404(t *testing.T) {
|
|
engine := newSPAEngine(t, true)
|
|
for _, path := range []string{
|
|
"/api/admin/v1/does-not-exist",
|
|
"/api/v1/nope",
|
|
"/swagger/admin/index.html",
|
|
"/static/missing.png",
|
|
"/ws/1/2",
|
|
"/info",
|
|
} {
|
|
if response := do(engine, http.MethodGet, path); response.Code != http.StatusNotFound {
|
|
t.Fatalf("%s 应返回 404,实际 %d", path, response.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 真实存在的接口不能被 SPA 回退影响。
|
|
func TestRegisteredAPIStillWorks(t *testing.T) {
|
|
engine := newSPAEngine(t, true)
|
|
if response := do(engine, http.MethodGet, "/api/admin/v1/ping"); response.Code != http.StatusOK {
|
|
t.Fatalf("已注册的接口被影响了: %d", response.Code)
|
|
}
|
|
}
|
|
|
|
// 非 GET 请求不该拿到 HTML——POST 到不存在的路径就是 404。
|
|
func TestNonGetDoesNotFallBack(t *testing.T) {
|
|
engine := newSPAEngine(t, true)
|
|
if response := do(engine, http.MethodPost, "/whatever"); response.Code != http.StatusNotFound {
|
|
t.Fatalf("POST 不应回退到 index.html,实际 %d", response.Code)
|
|
}
|
|
}
|
|
|
|
// 开发环境没有 dist,绝不能注册 NoRoute——否则每个真 404 都变成一张 HTML。
|
|
func TestWithoutDistNoFallbackIsInstalled(t *testing.T) {
|
|
engine := newSPAEngine(t, false)
|
|
if response := do(engine, http.MethodGet, "/goauto/syb-products"); response.Code != http.StatusNotFound {
|
|
t.Fatalf("没有 dist 时不应回退,实际 %d", response.Code)
|
|
}
|
|
if response := do(engine, http.MethodGet, "/api/admin/v1/ping"); response.Code != http.StatusOK {
|
|
t.Fatalf("没有 dist 时接口仍应正常: %d", response.Code)
|
|
}
|
|
}
|
|
|
|
// #346: with a built frontend, `/` must be the Admin SPA — never go-admin's
|
|
// welcome page, even in non-prod modes where the welcome page used to own `/`.
|
|
func TestRootServesSPAWhenDistExists(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
dist := filepath.Join(t.TempDir(), "dist")
|
|
if err := os.MkdirAll(dist, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("<!doctype html>SPA"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(SPADirEnv, dist)
|
|
for _, mode := range []string{"dev", "test", "prod"} {
|
|
engine := gin.New()
|
|
registerRootRoute(engine, mode, func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") })
|
|
InitSPARouter(engine)
|
|
response := do(engine, http.MethodGet, "/")
|
|
if response.Code != http.StatusOK || response.Body.String() != "<!doctype html>SPA" {
|
|
t.Fatalf("mode=%s: / should serve index.html, got %d %q", mode, response.Code, response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Without a dist (development under vite) the previous behaviour is kept:
|
|
// welcome page outside prod, nothing registered in prod.
|
|
func TestRootWithoutDistKeepsPreviousBehaviour(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
t.Setenv(SPADirEnv, filepath.Join(t.TempDir(), "missing-dist"))
|
|
welcome := func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") }
|
|
|
|
dev := gin.New()
|
|
registerRootRoute(dev, "dev", welcome)
|
|
if response := do(dev, http.MethodGet, "/"); response.Code != http.StatusOK || response.Body.String() != "GO-ADMIN欢迎您" {
|
|
t.Fatalf("dev without dist should keep the welcome page, got %d %q", response.Code, response.Body.String())
|
|
}
|
|
|
|
prod := gin.New()
|
|
registerRootRoute(prod, "prod", welcome)
|
|
if response := do(prod, http.MethodGet, "/"); response.Code != http.StatusNotFound {
|
|
t.Fatalf("prod without dist should not register /, got %d", response.Code)
|
|
}
|
|
}
|