package server import ( "os" "path/filepath" "testing" "gitcat/internal/gitx" ) // TestRenameRepoKeepsDiskInSync 是 P0-2 的回归测试:改名后数据库与磁盘目录 // 必须同时指向新名字。修复前 UpdateRepo 会改 DB 名却不改目录,会把仓库 // 变成「数据缺失」。 func TestRenameRepoKeepsDiskInSync(t *testing.T) { srv, st, alice, _ := newTestServer(t) oldDir := makeRepo(t, srv, st, "oldname", alice) repo, err := st.RepoByName("oldname") if err != nil { t.Fatalf("读仓库失败: %v", err) } if err := srv.renameRepo(repo, "newname"); err != nil { t.Fatalf("改名失败: %v", err) } if _, err := os.Stat(oldDir); !os.IsNotExist(err) { t.Error("旧目录仍在,磁盘未被同步重命名") } newDir := srv.repoPath("newname") if info, err := os.Stat(filepath.Join(newDir, "HEAD")); err != nil || info.IsDir() { t.Errorf("新目录不是有效的裸仓库: %v", err) } if !gitx.IsBareRepo(newDir) { t.Error("改名后无法被识别为裸仓库") } got, err := st.RepoByName("newname") if err != nil { t.Fatalf("按新名查不到仓库: %v", err) } if got.ID != repo.ID { t.Errorf("改名后 ID 变了: %d -> %d", repo.ID, got.ID) } if _, err := os.Stat(srv.repoPath(got.Name) + "/HEAD"); err != nil { t.Errorf("改名后 DB 与磁盘脱钩: %v", err) } } func TestRenameToExistingNameFails(t *testing.T) { srv, st, alice, _ := newTestServer(t) dirA := makeRepo(t, srv, st, "alpha", alice) dirB := makeRepo(t, srv, st, "beta", alice) repo, err := st.RepoByName("alpha") if err != nil { t.Fatalf("读仓库失败: %v", err) } if err := srv.renameRepo(repo, "beta"); err == nil { t.Fatal("改名到已存在名称应失败") } // 失败后两侧都必须保持原样 if _, err := os.Stat(filepath.Join(dirA, "HEAD")); err != nil { t.Errorf("失败的改名破坏了源目录: %v", err) } if _, err := os.Stat(filepath.Join(dirB, "HEAD")); err != nil { t.Errorf("失败的改名破坏了目标目录: %v", err) } if got, _ := st.RepoByName("alpha"); got == nil { t.Error("失败的改名却改了数据库") } } func TestRenameRejectsIllegalNames(t *testing.T) { srv, st, alice, _ := newTestServer(t) makeRepo(t, srv, st, "keep", alice) bad := []string{"", " ", "../escape", "a/b", `a\b`, "has space", "admin", "-lead", ".hidden", "a..b", ".git", "a.git.git", "x" + string(rune(0))} for _, name := range bad { if got, err := validateNewRepoName(name); err == nil { t.Errorf("非法名 %q 通过了校验,得到 %q", name, got) } } // 与「新建项目」一致:输入带 .git 后缀时会被剥掉,而不是报错 if got, err := validateNewRepoName("proj.git"); err != nil || got != "proj" { t.Errorf("带 .git 后缀应被接受并剥离,得到 %q(err=%v)", got, err) } if got, err := validateNewRepoName("good-name.v2"); err != nil || got != "good-name.v2" { t.Errorf("合法名被误拒: got=%q err=%v", got, err) } if _, err := os.Stat(srv.repoPath("keep") + "/HEAD"); err != nil { t.Errorf("非法改名破坏了目录: %v", err) } if got, _ := st.RepoByName("keep"); got == nil { t.Error("非法改名却改了数据库") } } // TestUpdateRepoCannotRename 锁死根因:普通更新不得改名字。 func TestUpdateRepoCannotRename(t *testing.T) { srv, st, _, _ := newTestServer(t) makeRepo(t, srv, st, "solid", nil) repo, _ := st.RepoByName("solid") repo.Name = "hijacked" repo.Description = "改描述" if err := st.UpdateRepo(repo); err != nil { t.Fatalf("UpdateRepo 失败: %v", err) } got, err := st.RepoByName("solid") if err != nil { t.Fatalf("原名查不到仓库(说明 UpdateRepo 改了名字): %v", err) } if got.Description != "改描述" { t.Errorf("描述未更新: %q", got.Description) } if _, err := st.RepoByName("hijacked"); err == nil { t.Error("UpdateRepo 竟然把仓库改名了") } if _, err := os.Stat(srv.repoPath("solid") + "/HEAD"); err != nil { t.Errorf("磁盘目录被动过: %v", err) } } // TestRenameKeepsPushHistory 改名不应影响已有的推送留痕。 func TestRenameKeepsPushHistory(t *testing.T) { srv, st, alice, _ := newTestServer(t) makeRepo(t, srv, st, "proj", alice) repo, _ := st.RepoByName("proj") if _, err := st.CreatePush(repo.ID, &alice.ID, alice.Username, "Alice", "main", "0000", "1111", nil); err != nil { t.Fatalf("造推送记录失败: %v", err) } if err := srv.renameRepo(repo, "proj-renamed"); err != nil { t.Fatalf("改名失败: %v", err) } pushes, err := st.PushesByRepo(repo.ID, 10) if err != nil { t.Fatalf("读推送记录失败: %v", err) } if len(pushes) != 1 { t.Fatalf("改名后推送记录丢失,剩 %d 条", len(pushes)) } if pushes[0].RepoName != "proj-renamed" { t.Errorf("推送记录里的仓库名未跟着更新: %q", pushes[0].RepoName) } }