package store import ( "os" "path/filepath" "testing" ) // TestOpenFreshInstall 验证全新安装能建表并写入版本号。 func TestOpenFreshInstall(t *testing.T) { dir := t.TempDir() st, err := Open(filepath.Join(dir, "gitcat.db")) if err != nil { t.Fatalf("打开新库失败: %v", err) } defer st.Close() if got := st.SchemaVersion(); got != SchemaVersion { t.Fatalf("全新库版本 = %d, 期望 %d", got, SchemaVersion) } if st.Installed() { t.Fatal("全新库不应处于已安装状态") } } // TestLegacyDatabaseUpgrade 模拟旧版本数据库:有表、无 schema_meta、缺索引。 // 这是"兼容旧版本"的核心保证——旧库必须能被新二进制直接打开。 func TestLegacyDatabaseUpgrade(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "gitcat.db") legacy, err := Open(path) if err != nil { t.Fatalf("准备旧库失败: %v", err) } // 造出旧版本的样子:写一条数据,然后抹掉 schema_meta 与新增的索引。 hash, err := legacy.CreateUser("alice", "Alice", "a@example.com", "x", true) if err != nil { t.Fatalf("建用户失败: %v", err) } if _, err := legacy.CreateRepo("demo", "旧项目", hash.ID); err != nil { t.Fatalf("建仓库失败: %v", err) } for _, stmt := range []string{ `DROP TABLE schema_meta`, `DROP INDEX IF EXISTS idx_repos_owner`, `DROP INDEX IF EXISTS idx_repos_updated`, `DROP INDEX IF EXISTS idx_pushes_user`, `DROP INDEX IF EXISTS idx_sessions_user`, } { if _, err := legacy.db.Exec(stmt); err != nil { t.Fatalf("构造旧库失败 (%s): %v", stmt, err) } } legacy.Close() // 关键:数据文件还在,用新二进制重新打开。 st, err := Open(path) if err != nil { t.Fatalf("新版本无法打开旧数据库: %v", err) } defer st.Close() if got := st.SchemaVersion(); got != SchemaVersion { t.Errorf("升级后版本 = %d, 期望 %d", got, SchemaVersion) } // 旧数据必须完好 if u, err := st.UserByUsername("ALICE"); err != nil || u.DisplayName != "Alice" { t.Errorf("旧用户丢失或查询失败: %v", err) } if r, err := st.RepoByName("demo"); err != nil || r.Description != "旧项目" { t.Errorf("旧仓库丢失或查询失败: %v", err) } // 索引必须被补上 for _, idx := range []string{"idx_repos_owner", "idx_repos_updated", "idx_pushes_user", "idx_sessions_user"} { var name string err := st.db.QueryRow(`SELECT name FROM sqlite_master WHERE type='index' AND name=?`, idx).Scan(&name) if err != nil { t.Errorf("索引 %s 未被创建: %v", idx, err) } } // 迁移前的自动备份 backups, _ := filepath.Glob(filepath.Join(dir, backupDir, "*.db")) if len(backups) == 0 { t.Error("迁移前应生成数据库备份") } } // TestDowngradeRefused 验证降级保护:数据库版本高于程序时拒绝启动。 func TestDowngradeRefused(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "gitcat.db") st, err := Open(path) if err != nil { t.Fatalf("准备库失败: %v", err) } if _, err := st.db.Exec(`UPDATE schema_meta SET value=? WHERE key='schema_version'`, itoa(SchemaVersion+5)); err != nil { t.Fatalf("抬高版本号失败: %v", err) } st.Close() if _, err := Open(path); err == nil { t.Fatal("数据库版本高于程序时应拒绝启动") } } // TestUpdateRepoKeepsName 验证 UpdateRepo 不再改名字—— // 名字变更必须走 RenameRepo,否则磁盘目录会与数据库脱钩。 func TestUpdateRepoKeepsName(t *testing.T) { st, err := Open(filepath.Join(t.TempDir(), "gitcat.db")) if err != nil { t.Fatalf("打开库失败: %v", err) } defer st.Close() owner, err := st.CreateUser("owner", "Owner", "", "x", false) if err != nil { t.Fatalf("建用户失败: %v", err) } r, err := st.CreateRepo("keepme", "描述", owner.ID) if err != nil { t.Fatalf("建仓库失败: %v", err) } r.Name = "hijacked" r.Description = "新描述" if err := st.UpdateRepo(r); err != nil { t.Fatalf("UpdateRepo 失败: %v", err) } got, err := st.RepoByID(r.ID) if err != nil { t.Fatalf("读取失败: %v", err) } if got.Name != "keepme" { t.Fatalf("UpdateRepo 意外改了仓库名: %q", got.Name) } if got.Description != "新描述" { t.Fatalf("描述未更新: %q", got.Description) } // 显式改名才生效 if err := st.RenameRepo(r.ID, "renamed"); err != nil { t.Fatalf("RenameRepo 失败: %v", err) } if got, _ := st.RepoByName("renamed"); got == nil { t.Fatal("RenameRepo 未生效") } // 大小写不敏感的唯一性仍然被遵守 if _, err := st.CreateRepo("taken", "", owner.ID); err != nil { t.Fatalf("建第二个仓库失败: %v", err) } if err := st.RenameRepo(r.ID, "TAKEN"); err == nil { t.Fatal("重名(大小写不同)应被拒绝") } } func itoa(n int) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var b []byte for n > 0 { b = append([]byte{byte('0' + n%10)}, b...) n /= 10 } if neg { return "-" + string(b) } return string(b) } var _ = os.Stat // 保持 os 导入(Windows 下 TempDir 清理相关)