package gateway import ( "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" ) // adminEntryTestIndex 是最小的 index.html:注入点 `` 与 // 站点根锚点都由 handler 追加,因此这里只保证两者都存在。 const adminEntryTestIndex = `
` // adminEntryEnv 构造一个挂了前端、并按 adminPath 配置安全入口的网关。 func adminEntryEnv(t *testing.T, adminPath string) *httptest.Server { t.Helper() cfg := cfgDefault() cfg.Server.AdminPath = adminPath front := fstest.MapFS{ "index.html": &fstest.MapFile{Data: []byte(adminEntryTestIndex)}, "assets/app.js": &fstest.MapFile{Data: []byte("var x = 1;")}, } _, ts, _ := buildEnv(t, cfg, Deps{Frontend: front}) return ts } // TestAdminEntry_DefaultPrefix 守住默认前缀下的安全入口(裁决 1.6/16.4.7): // `/admin` 与深链直达面板入口页(带注入),不再回落用户端聊天壳;用户端 // 路径与真实静态资源不受影响。 func TestAdminEntry_DefaultPrefix(t *testing.T) { t.Parallel() ts := adminEntryEnv(t, "") resp, body := doReq(t, http.MethodGet, ts.URL+"/admin", nil, "") if resp.StatusCode != http.StatusOK { t.Fatalf("GET /admin status = %d, want 200 (body=%s)", resp.StatusCode, body) } if !strings.Contains(string(body), `window.__CYRENE_ADMIN_ENTRY__={"prefix":"/admin"}`) { t.Fatalf("GET /admin missing injected entry info: %s", body) } if !strings.Contains(string(body), ``) { t.Fatalf("GET /admin missing root base href: %s", body) } if got := resp.Header.Get("Cache-Control"); got != "no-store" { t.Fatalf("GET /admin Cache-Control = %q, want no-store", got) } resp, body = doReq(t, http.MethodGet, ts.URL+"/admin/personas", nil, "") if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"prefix":"/admin"`) { t.Fatalf("GET /admin/personas = %d body=%s, want the injected admin entry", resp.StatusCode, body) } // 真实静态资源仍按文件送出(不注入)。 resp, body = doReq(t, http.MethodGet, ts.URL+"/assets/app.js", nil, "") if resp.StatusCode != http.StatusOK || string(body) != "var x = 1;" { t.Fatalf("GET /assets/app.js = %d body=%q, want the raw asset", resp.StatusCode, body) } // 用户端回落路径不带入口信息(用户端 bundle 与页面都不该出现面板前缀)。 resp, body = doReq(t, http.MethodGet, ts.URL+"/chat/somewhere", nil, "") if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "root") { t.Fatalf("GET /chat/somewhere = %d body=%s, want the user shell", resp.StatusCode, body) } if strings.Contains(string(body), "__CYRENE_ADMIN_ENTRY__") { t.Fatalf("user shell leaked the admin entry info: %s", body) } } // TestAdminEntry_CustomPrefix 守住自定义前缀(`host/xxx/admin` 形态):正确 // 前缀直达,猜错前缀(含默认 /admin)一律 404,绝不回落用户端聊天壳。 func TestAdminEntry_CustomPrefix(t *testing.T) { t.Parallel() ts := adminEntryEnv(t, "xxx/admin") resp, body := doReq(t, http.MethodGet, ts.URL+"/xxx/admin", nil, "") if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"prefix":"/xxx/admin"`) { t.Fatalf("GET /xxx/admin = %d body=%s, want the injected custom prefix", resp.StatusCode, body) } for _, p := range []string{"/admin", "/admin/personas", "/other/admin", "/admin/"} { resp, body = doReq(t, http.MethodGet, ts.URL+p, nil, "") if resp.StatusCode != http.StatusNotFound { t.Fatalf("GET %s = %d body=%s, want 404 (wrong prefix must not fall back)", p, resp.StatusCode, body) } if strings.Contains(string(body), "__CYRENE_ADMIN_ENTRY__") { t.Fatalf("GET %s leaked the admin entry info", p) } } // 用户端路径依旧正常。 resp, body = doReq(t, http.MethodGet, ts.URL+"/chat", nil, "") if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "root") { t.Fatalf("GET /chat = %d body=%s, want the user shell", resp.StatusCode, body) } } // TestAdminEntry_APIRouteUntouched 守住「API 不藏」:前缀即便被配成含 api 段 // 的值,也不得截住 REST 路由(design/07 §0.1)。 func TestAdminEntry_APIRouteUntouched(t *testing.T) { t.Parallel() ts := adminEntryEnv(t, "api/v1/admin") resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/health", nil, "") if resp.StatusCode != http.StatusOK { t.Fatalf("GET /api/v1/health = %d body=%s, want the health payload", resp.StatusCode, body) } if strings.Contains(string(body), "__CYRENE_ADMIN_ENTRY__") { t.Fatalf("API route was served the admin entry page: %s", body) } } // TestAdminEntry_PrefixOverAssetDir 守住前缀撞上真实资源目录的退化:把前缀 // 配成 assets 时,真实资源必须仍由文件服务器送出,不能被面板入口页顶掉。 func TestAdminEntry_PrefixOverAssetDir(t *testing.T) { t.Parallel() ts := adminEntryEnv(t, "assets") resp, body := doReq(t, http.MethodGet, ts.URL+"/assets/app.js", nil, "") if resp.StatusCode != http.StatusOK || string(body) != "var x = 1;" { t.Fatalf("GET /assets/app.js = %d body=%q, want the real asset (not the shell)", resp.StatusCode, body) } } // TestAdminEntryHTML_NoHead 守住注入的两个回退点:没有 `` 时注入到 // 文档最前(仍先于任何资源引用),且 `` 与入口信息一并出现。 func TestAdminEntryHTML_NoHead(t *testing.T) { t.Parallel() out := adminEntryHTML([]byte("x"), "/admin") s := string(out) if !strings.HasPrefix(s, ``) { t.Fatalf("injection without = %q, want it prepended", s) } if !strings.Contains(s, `"prefix":"/admin"`) { t.Fatalf("injection without lost the entry info: %s", s) } }