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)
}
}