Files
RawMark/internal/api/themes_test.go
T
eezd a716794e41 feat(ui): improve readability and fix workspace regressions
Separate interface typography from document text and enlarge controls across the workspace and public reader. Keep tree menus within the viewport and make constrained dialogs scrollable.

Preserve nested image references, escape moved link paths, synchronize note creation and daily-note opening, and hydrate shared headings. Add regression coverage for settings and workspace behavior.
2026-10-23 10:55:53 +08:00

83 lines
3.5 KiB
Go

package api
import (
"encoding/json"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
func TestThemeAndShareAppearanceBoundaries(t *testing.T) {
f := newFixture(t)
dir := filepath.Join(filepath.Dir(f.root), "themes", "paper")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
for name, data := range map[string]string{
"manifest.json": `{"id":"paper","name":"纸张","version":"1"}`,
"theme.css": `:root { --rm-bg: #faf6ed; }`,
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(data), 0o644); err != nil {
t.Fatal(err)
}
}
guest := newClient()
if code := f.status(guest, "GET", "/api/v1/themes/paper", "", nil); code != 401 {
t.Fatalf("匿名用户不应读取主题,得到 %d", code)
}
themes := decode[struct{ Themes []themeManifest }](t, f.do(f.admin, "GET", "/api/v1/themes", "", nil))
if len(themes.Themes) != 4 || themes.Themes[0].ID != "default" || themes.Themes[1].ID != "white" || themes.Themes[2].ID != "black" || themes.Themes[3].ID != "paper" {
t.Fatalf("主题列表错误:%+v", themes.Themes)
}
for _, id := range []string{"white", "black"} {
theme := decode[map[string]string](t, f.do(f.admin, "GET", "/api/v1/themes/"+id, "", nil))
if theme["id"] != id || !strings.Contains(theme["css"], "--rm-bg:") {
t.Fatalf("内置主题未提供配色:%s", id)
}
}
theme := decode[map[string]string](t, f.do(f.admin, "GET", "/api/v1/themes/paper", "", nil))
if !strings.Contains(theme["css"], "#faf6ed") {
t.Fatal("未读取到主题 CSS")
}
f.write("笔记.md", "正文")
f.do(f.admin, "PATCH", "/api/v1/settings", `{"theme":"paper","colorMode":"dark","fontSize":18,"uiFontSize":20,"privateSetting":"不公开","cssSnippets":[{"id":"a","name":"启用","css":"body { color: red }","enabled":true},{"id":"b","name":"停用","css":"不公开片段","enabled":false}]}`, nil)
sh := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", `{"path":"笔记.md","password":"访问密码"}`, nil))
base := "/api/v1/public/shares/" + sh.Token
if code := f.status(guest, "GET", base+"/appearance", "", nil); code != 401 {
t.Fatalf("未解锁不能读取外观,得到 %d", code)
}
f.do(guest, "POST", base+"/unlock", `{"password":"访问密码"}`, nil)
res := f.do(guest, "GET", base+"/appearance", "", nil)
raw, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
var appearance map[string]any
if err := json.Unmarshal(raw, &appearance); err != nil {
t.Fatal(err)
}
if res.StatusCode != 200 || appearance["colorMode"] != "dark" || appearance["uiFontSize"] != float64(20) || appearance["fontSize"] != float64(18) || !strings.Contains(appearance["themeCSS"].(string), "#faf6ed") {
t.Fatalf("分享外观错误:%s", raw)
}
if strings.Contains(string(raw), "不公开") || len(appearance["cssSnippets"].([]any)) != 1 {
t.Fatalf("分享泄露了其他设置或停用片段:%s", raw)
}
f.do(f.admin, "DELETE", "/api/v1/shares/"+sh.Token, "", nil)
if code := f.status(guest, "GET", base+"/appearance", "", nil); code != 404 {
t.Fatalf("撤销分享后不能读取外观,得到 %d", code)
}
// CSS 符号链接不能读取主题目录之外的应用数据。
if err := os.Remove(filepath.Join(dir, "theme.css")); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(filepath.Dir(f.root), "rawmark.db"), filepath.Join(dir, "theme.css")); err != nil {
t.Fatal(err)
}
if code := f.status(f.admin, "GET", "/api/v1/themes/paper", "", nil); code != http.StatusInternalServerError {
t.Fatalf("应拒绝越界链接,得到 %d", code)
}
}