142 lines
4.9 KiB
Go
142 lines
4.9 KiB
Go
package main
|
||
|
||
import (
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 项目/规则文档的生成与套用(不依赖 MySQL 的本地部分)。
|
||
// v2 语义:身份文档只按 name 匹配更新,不再从文档落库项目;
|
||
// 项目落地依赖 paths:<machineID> 行(applyMachinePaths)或用户手动绑定。
|
||
func TestProjectsDocRoundTrip(t *testing.T) {
|
||
dir := t.TempDir()
|
||
s, e := OpenStore(filepath.Join(dir, "docs.db"))
|
||
if e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
defer s.db.Close()
|
||
a := NewApp()
|
||
a.store = s
|
||
|
||
doc, e := a.projectsDoc()
|
||
if e != nil || doc != "[]" {
|
||
t.Fatalf("empty doc want [], got %q err=%v", doc, e)
|
||
}
|
||
|
||
// 远端身份文档(v1 旧格式,元素带 path):本机没有这些项目 → 全部进待绑定,不落库。
|
||
p1 := filepath.Join(dir, "alpha")
|
||
p2 := filepath.Join(dir, "beta")
|
||
remote := `[{"name":"Alpha","path":` + jsonStr(p1) + `,"description":"d1","group":"Work","favorite":true},` +
|
||
`{"name":"Beta","path":` + jsonStr(p2) + `}]`
|
||
if e := a.applyProjectsDoc(remote); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
var n int
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
|
||
if n != 0 {
|
||
t.Fatalf("identity doc must not insert projects, got %d", n)
|
||
}
|
||
if pending, _ := a.ListCloudPendingProjects(); len(pending) != 2 {
|
||
t.Fatalf("expect 2 pending cloud projects, got %d", len(pending))
|
||
}
|
||
|
||
// 本机 paths 行到达(同机换库场景)→ 项目落库,随后身份文档补齐分组/收藏。
|
||
if e := a.applyMachinePaths(`{"Alpha":` + jsonStr(p1) + `,"Beta":` + jsonStr(p2) + `}`); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
if e := a.applyProjectsDoc(remote); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
var gid int64
|
||
if e := s.db.QueryRow(`SELECT group_id FROM projects WHERE path=?`, p1).Scan(&gid); e != nil || gid == 1 {
|
||
t.Fatalf("alpha should be in new group, gid=%d err=%v", gid, e)
|
||
}
|
||
var gname string
|
||
_ = s.db.QueryRow(`SELECT name FROM project_groups WHERE id=?`, gid).Scan(&gname)
|
||
if gname != "Work" {
|
||
t.Fatalf("group name want Work, got %q", gname)
|
||
}
|
||
var fav int
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM favorites f JOIN projects p ON p.id=f.project_id WHERE p.path=?`, p1).Scan(&fav)
|
||
if fav != 1 {
|
||
t.Fatal("alpha should be favorite")
|
||
}
|
||
if pending, _ := a.ListCloudPendingProjects(); len(pending) != 0 {
|
||
t.Fatalf("pending should be empty after paths applied, got %d", len(pending))
|
||
}
|
||
|
||
// 再生成文档应包含两个项目且不带 path;重复套用幂等。
|
||
doc, e = a.projectsDoc()
|
||
if e != nil || !strings.Contains(doc, "Alpha") || !strings.Contains(doc, "Beta") {
|
||
t.Fatalf("doc missing entries: %q err=%v", doc, e)
|
||
}
|
||
if strings.Contains(doc, `"path"`) {
|
||
t.Fatalf("v2 doc must not contain path: %q", doc)
|
||
}
|
||
if e := a.applyProjectsDoc(remote); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
|
||
if n != 2 {
|
||
t.Fatalf("apply should be idempotent, projects=%d", n)
|
||
}
|
||
|
||
// 更新:改名(v1 文档按 path 匹配到本地项目)+ 取消收藏;本地多出的项目不受影响(只增改不删)。
|
||
remote2 := `[{"name":"Alpha2","path":` + jsonStr(p1) + `,"description":"d2","group":"Work","favorite":false}]`
|
||
if e := a.applyProjectsDoc(remote2); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
var name string
|
||
_ = s.db.QueryRow(`SELECT name FROM projects WHERE path=?`, p1).Scan(&name)
|
||
if name != "Alpha2" {
|
||
t.Fatalf("want renamed Alpha2, got %q", name)
|
||
}
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM favorites f JOIN projects p ON p.id=f.project_id WHERE p.path=?`, p1).Scan(&fav)
|
||
if fav != 0 {
|
||
t.Fatal("favorite should be removed")
|
||
}
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
|
||
if n != 2 {
|
||
t.Fatalf("pull must not delete local projects, got %d", n)
|
||
}
|
||
}
|
||
|
||
func TestRulesDocRoundTrip(t *testing.T) {
|
||
s, e := OpenStore(filepath.Join(t.TempDir(), "rules.db"))
|
||
if e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
defer s.db.Close()
|
||
a := NewApp()
|
||
a.store = s
|
||
|
||
if _, e := s.AddRule("*.bak", "custom"); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
doc, e := a.rulesDoc()
|
||
if e != nil || !strings.Contains(doc, "*.bak") {
|
||
t.Fatalf("doc should contain custom rule: %q err=%v", doc, e)
|
||
}
|
||
var builtinBefore int
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE builtin=1`).Scan(&builtinBefore)
|
||
|
||
// 远端替换自定义部分:*.bak 消失,*.tmp 出现,内置规则数不变。
|
||
if e := a.applyRulesDoc(`[{"pattern":"*.tmp","category":"custom"}]`); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
var nBak, nTmp, builtinAfter int
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE pattern='*.bak'`).Scan(&nBak)
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE pattern='*.tmp' AND builtin=0`).Scan(&nTmp)
|
||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE builtin=1`).Scan(&builtinAfter)
|
||
if nBak != 0 || nTmp != 1 || builtinAfter != builtinBefore {
|
||
t.Fatalf("replace failed: bak=%d tmp=%d builtin %d->%d", nBak, nTmp, builtinBefore, builtinAfter)
|
||
}
|
||
}
|
||
|
||
// jsonStr 把路径安全编码为 JSON 字符串(Windows 反斜杠)。
|
||
func jsonStr(s string) string {
|
||
b := strings.ReplaceAll(s, `\`, `\\`)
|
||
return `"` + b + `"`
|
||
}
|