仰望星辰工作室

gitcat

gitcat/ internal/store/store_test.go 5.0 KB · 175 行 原始文件
1package store
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7)
8
9// TestOpenFreshInstall 验证全新安装能建表并写入版本号。
10func TestOpenFreshInstall(t *testing.T) {
11 dir := t.TempDir()
12 st, err := Open(filepath.Join(dir, "gitcat.db"))
13 if err != nil {
14 t.Fatalf("打开新库失败: %v", err)
15 }
16 defer st.Close()
17 if got := st.SchemaVersion(); got != SchemaVersion {
18 t.Fatalf("全新库版本 = %d, 期望 %d", got, SchemaVersion)
19 }
20 if st.Installed() {
21 t.Fatal("全新库不应处于已安装状态")
22 }
23}
24
25// TestLegacyDatabaseUpgrade 模拟旧版本数据库:有表、无 schema_meta、缺索引。
26// 这是"兼容旧版本"的核心保证——旧库必须能被新二进制直接打开。
27func TestLegacyDatabaseUpgrade(t *testing.T) {
28 dir := t.TempDir()
29 path := filepath.Join(dir, "gitcat.db")
30 legacy, err := Open(path)
31 if err != nil {
32 t.Fatalf("准备旧库失败: %v", err)
33 }
34 // 造出旧版本的样子:写一条数据,然后抹掉 schema_meta 与新增的索引。
35 hash, err := legacy.CreateUser("alice", "Alice", "a@example.com", "x", true)
36 if err != nil {
37 t.Fatalf("建用户失败: %v", err)
38 }
39 if _, err := legacy.CreateRepo("demo", "旧项目", hash.ID); err != nil {
40 t.Fatalf("建仓库失败: %v", err)
41 }
42 for _, stmt := range []string{
43 `DROP TABLE schema_meta`,
44 `DROP INDEX IF EXISTS idx_repos_owner`,
45 `DROP INDEX IF EXISTS idx_repos_updated`,
46 `DROP INDEX IF EXISTS idx_pushes_user`,
47 `DROP INDEX IF EXISTS idx_sessions_user`,
48 } {
49 if _, err := legacy.db.Exec(stmt); err != nil {
50 t.Fatalf("构造旧库失败 (%s): %v", stmt, err)
51 }
52 }
53 legacy.Close()
54
55 // 关键:数据文件还在,用新二进制重新打开。
56 st, err := Open(path)
57 if err != nil {
58 t.Fatalf("新版本无法打开旧数据库: %v", err)
59 }
60 defer st.Close()
61
62 if got := st.SchemaVersion(); got != SchemaVersion {
63 t.Errorf("升级后版本 = %d, 期望 %d", got, SchemaVersion)
64 }
65 // 旧数据必须完好
66 if u, err := st.UserByUsername("ALICE"); err != nil || u.DisplayName != "Alice" {
67 t.Errorf("旧用户丢失或查询失败: %v", err)
68 }
69 if r, err := st.RepoByName("demo"); err != nil || r.Description != "旧项目" {
70 t.Errorf("旧仓库丢失或查询失败: %v", err)
71 }
72 // 索引必须被补上
73 for _, idx := range []string{"idx_repos_owner", "idx_repos_updated", "idx_pushes_user", "idx_sessions_user"} {
74 var name string
75 err := st.db.QueryRow(`SELECT name FROM sqlite_master WHERE type='index' AND name=?`, idx).Scan(&name)
76 if err != nil {
77 t.Errorf("索引 %s 未被创建: %v", idx, err)
78 }
79 }
80 // 迁移前的自动备份
81 backups, _ := filepath.Glob(filepath.Join(dir, backupDir, "*.db"))
82 if len(backups) == 0 {
83 t.Error("迁移前应生成数据库备份")
84 }
85}
86
87// TestDowngradeRefused 验证降级保护:数据库版本高于程序时拒绝启动。
88func TestDowngradeRefused(t *testing.T) {
89 dir := t.TempDir()
90 path := filepath.Join(dir, "gitcat.db")
91 st, err := Open(path)
92 if err != nil {
93 t.Fatalf("准备库失败: %v", err)
94 }
95 if _, err := st.db.Exec(`UPDATE schema_meta SET value=? WHERE key='schema_version'`,
96 itoa(SchemaVersion+5)); err != nil {
97 t.Fatalf("抬高版本号失败: %v", err)
98 }
99 st.Close()
100
101 if _, err := Open(path); err == nil {
102 t.Fatal("数据库版本高于程序时应拒绝启动")
103 }
104}
105
106// TestUpdateRepoKeepsName 验证 UpdateRepo 不再改名字——
107// 名字变更必须走 RenameRepo,否则磁盘目录会与数据库脱钩。
108func TestUpdateRepoKeepsName(t *testing.T) {
109 st, err := Open(filepath.Join(t.TempDir(), "gitcat.db"))
110 if err != nil {
111 t.Fatalf("打开库失败: %v", err)
112 }
113 defer st.Close()
114
115 owner, err := st.CreateUser("owner", "Owner", "", "x", false)
116 if err != nil {
117 t.Fatalf("建用户失败: %v", err)
118 }
119
120 r, err := st.CreateRepo("keepme", "描述", owner.ID)
121 if err != nil {
122 t.Fatalf("建仓库失败: %v", err)
123 }
124 r.Name = "hijacked"
125 r.Description = "新描述"
126 if err := st.UpdateRepo(r); err != nil {
127 t.Fatalf("UpdateRepo 失败: %v", err)
128 }
129 got, err := st.RepoByID(r.ID)
130 if err != nil {
131 t.Fatalf("读取失败: %v", err)
132 }
133 if got.Name != "keepme" {
134 t.Fatalf("UpdateRepo 意外改了仓库名: %q", got.Name)
135 }
136 if got.Description != "新描述" {
137 t.Fatalf("描述未更新: %q", got.Description)
138 }
139
140 // 显式改名才生效
141 if err := st.RenameRepo(r.ID, "renamed"); err != nil {
142 t.Fatalf("RenameRepo 失败: %v", err)
143 }
144 if got, _ := st.RepoByName("renamed"); got == nil {
145 t.Fatal("RenameRepo 未生效")
146 }
147 // 大小写不敏感的唯一性仍然被遵守
148 if _, err := st.CreateRepo("taken", "", owner.ID); err != nil {
149 t.Fatalf("建第二个仓库失败: %v", err)
150 }
151 if err := st.RenameRepo(r.ID, "TAKEN"); err == nil {
152 t.Fatal("重名(大小写不同)应被拒绝")
153 }
154}
155
156func itoa(n int) string {
157 if n == 0 {
158 return "0"
159 }
160 neg := n < 0
161 if neg {
162 n = -n
163 }
164 var b []byte
165 for n > 0 {
166 b = append([]byte{byte('0' + n%10)}, b...)
167 n /= 10
168 }
169 if neg {
170 return "-" + string(b)
171 }
172 return string(b)
173}
174
175var _ = os.Stat // 保持 os 导入(Windows 下 TempDir 清理相关)