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