Files
goauto/server/app/admin/router/spa_test.go
T
QiuSWandClaude Opus 5 c86e98d0cb feat(#48): make one config work for both development and deployment
The repo already layered file defaults under environment overrides, on
both the backend (settings.yml < GOAUTO_*) and the frontend
(.env.production < process.env). What was missing was a translator for
production: config.yaml only ever existed for the PowerShell launchers,
so a packaged binary read none of it and had no database credentials
either — SYB was inheriting an existing gap, not creating one.

The server now reads config.yaml itself, between settings.yml and the
environment. Lookup is GOAUTO_CONFIG, then ./config.yaml, then beside the
executable, so a packaged binary works wherever it is started. An absent
file is not an error: containers supply everything through the
environment. Scalars are read by YAML type and coerced, so an unquoted
all-digit password cannot take startup down over a quoting detail.

This removed the need for a Read-SybConfig in PowerShell: the launcher
just hands over the path it already knows, rather than reimplementing a
YAML parser.

The server also serves the built frontend when dist is present, which is
what .env.production's empty VUE_APP_BASE_API already assumes. The
history fallback is restricted to non-API GETs, and is not installed at
all without dist, so development 404s stay 404s.

Precedence is mutation-tested: applying the local file after the
environment instead of before makes the layering test fail.

Not verified: the PowerShell change and any Windows deployment — both
need a run on the Windows side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:28:45 +08:00

94 lines
3.1 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)
}
}