Files
HomeDash/admin-server/main_test.go
HomeDash Bot 064f986a7f fix(theme): 深色遮罩交给服务端,避免双重压暗
实测发现深色下 body 的 background-image 仍是亮色的 70% 白遮罩(壁纸被洗白)。
遮罩由服务端 applyWallpaperBlock 写进 conf.yml,因此在那里补一条深色规则
(同一张壁纸 + rgba(6,12,24,dim+0.15) 遮罩),并移除 navi.css 里那层
body::before 遮罩,避免壁纸被双重压暗。加单测断言深浅两条规则共存、
且移除壁纸时一并清除。
2026-09-12 07:49:34 +08:00

264 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestBeianFooter(t *testing.T) {
cases := []struct {
name, icp, gongan string
wantParts []string
wantEmpty bool
}{
{name: "both", icp: "京ICP备00000000号-1", gongan: "京公网安备00000000000000号",
wantParts: []string{"class=\"navi-footer\"", "beian.miit.gov.cn", "京ICP备00000000号-1", "navi-footer-sep", "beian.mps.gov.cn", "京公网安备00000000000000号"}},
{name: "only icp", icp: " 京ICP备12345678号 ", wantParts: []string{"class=\"navi-footer\"", "京ICP备12345678号"}},
{name: "only gongan", gongan: "沪公网安备31010102002555号", wantParts: []string{"class=\"navi-footer\"", "沪公网安备31010102002555号"}},
{name: "blank", icp: " ", gongan: "", wantEmpty: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := beianFooter(tc.icp, tc.gongan)
if tc.wantEmpty {
if got != "" {
t.Fatalf("want empty footer, got %q", got)
}
return
}
for _, p := range tc.wantParts {
if !strings.Contains(got, p) {
t.Fatalf("footer %q missing %q", got, p)
}
}
if strings.Contains(got, " ") {
t.Fatalf("footer keeps stray whitespace: %q", got)
}
})
}
}
// 备案号里若混入引号/尖括号,必须被转义,不能原样拼进 HTML。
func TestBeianFooterEscapesInput(t *testing.T) {
got := beianFooter(`"><script>alert(1)</script>`, "")
if strings.Contains(got, "<script>") {
t.Fatalf("unescaped input leaked into footer: %q", got)
}
if !strings.Contains(got, "&lt;script&gt;") && !strings.Contains(got, "&#34;") {
t.Fatalf("expected escaped output, got %q", got)
}
}
func TestMapString(t *testing.T) {
m := map[string]any{"a": "x", "b": 1, "c": nil}
if got := mapString(m, "a"); got != "x" {
t.Fatalf("mapString(a) = %q; want x", got)
}
if got := mapString(m, "b"); got != "" {
t.Fatalf("mapString(b) = %q; want empty for non-string", got)
}
if got := mapString(m, "missing"); got != "" {
t.Fatalf("mapString(missing) = %q; want empty", got)
}
}
func TestFrameBlockReason(t *testing.T) {
cases := []struct {
name string
xfo string
csp string
want string
}{
{"无任何头 → 可内嵌", "", "", ""},
{"XFO SAMEORIGIN 拒绝", "SAMEORIGIN", "", "X-Frame-Options: SAMEORIGIN"},
{"XFO DENY 拒绝", "DENY", "", "X-Frame-Options: DENY"},
{"CSP frame-ancestors 'self' 拒绝", "", "default-src 'self'; frame-ancestors 'self'", "frame-ancestors 'self'"},
{"CSP frame-ancestors * 放行", "", "frame-ancestors *", ""},
{"CSP 其它指令不影响", "", "default-src 'self'; img-src https:", ""},
}
for _, c := range cases {
h := http.Header{}
if c.xfo != "" {
h.Set("X-Frame-Options", c.xfo)
}
if c.csp != "" {
h.Set("Content-Security-Policy", c.csp)
}
if got := frameBlockReason(h); got != c.want {
t.Errorf("%s: frameBlockReason() = %q; want %q", c.name, got, c.want)
}
}
}
func TestFrameableVerdict(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ok":
w.WriteHeader(http.StatusOK)
case "/xfo":
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.WriteHeader(http.StatusOK)
case "/waf":
// WAF 拦截:403 且不带任何 X-Frame-Options —— 这是之前误判的根源
w.WriteHeader(http.StatusForbidden)
case "/csp":
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
w.WriteHeader(http.StatusOK)
case "/redir":
http.Redirect(w, r, "https://example.invalid/login", http.StatusFound)
case "/echo-cookie":
if r.Header.Get("Cookie") == "" {
w.WriteHeader(http.StatusForbidden)
return
}
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.WriteHeader(http.StatusOK)
}
}))
defer srv.Close()
cases := []struct {
path string
frameable bool
conclusive bool
wantReasonPart string
}{
{"/ok", true, true, ""},
{"/xfo", false, true, "SAMEORIGIN"},
{"/csp", false, true, "frame-ancestors"},
{"/waf", true, false, "HTTP 403"},
{"/echo-cookie", false, true, "SAMEORIGIN"},
}
for _, c := range cases {
got := frameableVerdict(srv.URL+c.path, "portal_session=abc")
if got.Frameable != c.frameable || got.Conclusive != c.conclusive {
t.Errorf("%s: got frameable=%v conclusive=%v; want %v/%v",
c.path, got.Frameable, got.Conclusive, c.frameable, c.conclusive)
}
if c.wantReasonPart != "" && !strings.Contains(got.Reason, c.wantReasonPart) {
t.Errorf("%s: reason = %q; want containing %q", c.path, got.Reason, c.wantReasonPart)
}
}
}
func TestFrameableVerdictWithoutCookieIsInconclusive(t *testing.T) {
// 门户后面的应用对未登录请求会 302 到登录页;带了 Cookie 才拿到真正的响应头。
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Cookie") == "" {
w.WriteHeader(http.StatusForbidden)
return
}
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
withCookie := frameableVerdict(srv.URL, "portal_session=abc")
if withCookie.Frameable || !withCookie.Conclusive {
t.Fatalf("带 Cookie 应判为确定不可内嵌,got %+v", withCookie)
}
without := frameableVerdict(srv.URL, "")
if without.Conclusive {
t.Fatalf("无 Cookie 应判为无法确认,got %+v", without)
}
}
// 分组增删改:历史分组受保护;删非空分组必须显式 force。
func TestSectionHandlers(t *testing.T) {
dir := t.TempDir()
oldConf := confFile
confFile = filepath.Join(dir, "conf.yml")
defer func() { confFile = oldConf }()
orig := `appConfig: {}
pageInfo: {}
sections:
- name: 常用服务
icon: fas fa-star
items:
- title: A
url: https://a.example
- name: 历史访问
icon: fas fa-history
items: []
`
if err := os.WriteFile(confFile, []byte(orig), 0o644); err != nil {
t.Fatal(err)
}
call := func(h http.HandlerFunc, method, target, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, target, strings.NewReader(body))
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
// 新增
if rec := call(handleAddSection, http.MethodPost, "/api/section", `{"name":"新分组"}`); rec.Code != 200 {
t.Fatalf("新增分组应成功,got %d %s", rec.Code, rec.Body.String())
}
// 重名
if rec := call(handleAddSection, http.MethodPost, "/api/section", `{"name":"新分组"}`); rec.Code != 409 {
t.Fatalf("同名分组应 409,got %d", rec.Code)
}
// 不能手工创建历史分组
if rec := call(handleAddSection, http.MethodPost, "/api/section", `{"name":"历史访问"}`); rec.Code != 409 {
t.Fatalf("手工创建历史分组应 409,got %d", rec.Code)
}
// 改名
if rec := call(handleUpdateSection, http.MethodPost, "/api/section/update", `{"index":2,"name":"改名后"}`); rec.Code != 200 {
t.Fatalf("改名应成功,got %d %s", rec.Code, rec.Body.String())
}
// 历史分组不能改名 / 删除
if rec := call(handleUpdateSection, http.MethodPost, "/api/section/update", `{"index":1,"name":"X"}`); rec.Code != 409 {
t.Fatalf("改历史分组应 409,got %d", rec.Code)
}
if rec := call(handleDeleteSection, http.MethodDelete, "/api/section", `{"index":1}`); rec.Code != 409 {
t.Fatalf("删历史分组应 409,got %d", rec.Code)
}
// 非空分组不给 force 应 409
if rec := call(handleDeleteSection, http.MethodDelete, "/api/section", `{"index":0}`); rec.Code != 409 {
t.Fatalf("删非空分组应 409,got %d", rec.Code)
}
// 给 force 才删
if rec := call(handleDeleteSection, http.MethodDelete, "/api/section?force=1", `{"index":0}`); rec.Code != 200 {
t.Fatalf("force 删除应成功,got %d %s", rec.Code, rec.Body.String())
}
cfg, err := loadConfig()
if err != nil {
t.Fatal(err)
}
names := make([]string, 0, len(cfg.Sections))
for _, s := range cfg.Sections {
names = append(names, s.Name)
}
want := []string{"历史访问", "改名后"}
if len(names) != len(want) || names[0] != want[0] || names[1] != want[1] {
t.Fatalf("删除/改名后分组 = %v; want %v", names, want)
}
}
// 深色模式必须换一层深色遮罩,否则亮色遮罩会把壁纸洗白。
func TestApplyWallpaperBlockEmitsDarkScrim(t *testing.T) {
out := applyWallpaperBlock("", "/wallpapers/w.jpg", 0.5)
if !strings.Contains(out, "linear-gradient(rgba(255,255,255,0.50)") {
t.Fatalf("缺少亮色遮罩:\n%s", out)
}
if !strings.Contains(out, "html.navi-dark body.navi-dark") ||
!strings.Contains(out, "rgba(6,12,24,0.65)") {
t.Fatalf("缺少深色遮罩规则:\n%s", out)
}
if strings.Count(out, "/wallpapers/w.jpg") != 2 {
t.Fatalf("深浅两种模式都应引用同一张壁纸:\n%s", out)
}
// 移除壁纸时应把两种规则一起清掉
if got := applyWallpaperBlock(out, "", 0.5); strings.Contains(got, "navi-dark body") {
t.Fatalf("移除壁纸后仍残留深色规则:\n%s", got)
}
}