package repository import ( "errors" "fmt" "path/filepath" "testing" "git.pcux.cn/xuans/Scert/backend/internal/config" "git.pcux.cn/xuans/Scert/backend/internal/database" "git.pcux.cn/xuans/Scert/backend/internal/domain" "git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest" ) func TestProjectDirectorySQLite(t *testing.T) { cfg := config.Default(t.TempDir()).Database cfg.DSN = filepath.Join(t.TempDir(), "project-directory.db") checkProjectDirectory(t, cfg) } func TestProjectDirectoryMySQLReal(t *testing.T) { server := mysqltest.New(t) checkProjectDirectory(t, server.Database(t)) } func checkProjectDirectory(t *testing.T, cfg config.DatabaseConfig) { t.Helper() db, err := database.Open(t.Context(), cfg) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) if err := database.Migrate(t.Context(), db.DB, cfg.Driver); err != nil { t.Fatal(err) } for _, user := range []struct{ id, role string }{ {"root", "super_admin"}, {"manager", "user"}, {"global-admin", "admin"}, {"viewer", "user"}, } { if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, `INSERT INTO users (id, username, password_hash, role, status) VALUES (?, ?, 'test-only-no-login', ?, 'active')`), user.id, user.id, user.role); err != nil { t.Fatal(err) } } projects := NewSQLProjectRepository(db.DB, cfg.Driver) repo := NewSQLProjectMemberRepository(db.DB, cfg.Driver) for i := 0; i < 12; i++ { id := fmt.Sprintf("visible-%02d", i) if err := projects.CreateProject(t.Context(), domain.Project{ID: id, Name: "测试项目", Description: "项目说明"}); err != nil { t.Fatal(err) } for _, role := range []domain.ProjectRole{domain.ProjectRoleAdmin, domain.ProjectRoleReadOnly} { if err := repo.GrantProjectRole(t.Context(), "root", id, "manager", role); err != nil { t.Fatal(err) } } } if err := repo.GrantProjectRole(t.Context(), "root", "visible-00", "viewer", domain.ProjectRoleReadOnly); err != nil { t.Fatal(err) } t.Run("visible_roles_and_empty_directory", func(t *testing.T) { for _, tc := range []struct { actor string count int manage bool }{ {"root", 13, true}, {"manager", 12, true}, {"viewer", 1, false}, {"global-admin", 0, false}, } { page, err := repo.ListVisibleProjects(t.Context(), tc.actor, "", 30) if err != nil || page.Items == nil || len(page.Items) != tc.count || page.HasMore || page.NextCursor != "" { t.Fatalf("项目可见性错误:%s,%+v,%v", tc.actor, page, err) } if page.CanCreateProject != (tc.actor == "root") { t.Fatalf("空目录也必须返回正确创建能力:%s,%+v", tc.actor, page) } for _, item := range page.Items { if item.CanManageProject != tc.manage || item.CanDeleteProject != (tc.manage && item.ID != domain.DefaultProjectID) { t.Fatalf("生命周期能力错误:%s,%+v,%+v", tc.actor, page, item) } if item.CanManageMembers != tc.manage || (tc.actor != "root" && item.ID == domain.DefaultProjectID) { t.Fatalf("全局身份被错误映射为项目权限:%s,%+v", tc.actor, item) } } } }) t.Run("pagination_deduplicates_roles", func(t *testing.T) { for _, size := range []int{1, 5, 10, 15, 30} { after := "" seen := make(map[string]bool) for attempts := 0; ; attempts++ { if attempts > 12 { t.Fatal("项目分页游标没有前进") } page, err := repo.ListVisibleProjects(t.Context(), "manager", after, size) if err != nil || len(page.Items) > size || page.PageSize != size { t.Fatalf("项目分页无效:%+v,%v", page, err) } for _, item := range page.Items { if seen[item.ID] || item.ID <= after { t.Fatalf("项目重复或排序错误:%+v", item) } seen[item.ID] = true } if !page.HasMore { break } if page.NextCursor != page.Items[len(page.Items)-1].ID { t.Fatal("下一页游标不是最后项目") } after = page.NextCursor } if len(seen) != 12 { t.Fatalf("项目被截断或泄露:%d", len(seen)) } } page, err := repo.ListVisibleProjects(t.Context(), "manager", "zzzz", 5) if err != nil || page.Items == nil || len(page.Items) != 0 { t.Fatalf("有效空页被拒绝:%+v,%v", page, err) } }) t.Run("realtime_revocation_and_disabled_accounts", func(t *testing.T) { if err := repo.RevokeProjectRole(t.Context(), "root", "visible-00", "manager", domain.ProjectRoleAdmin); err != nil { t.Fatal(err) } page, err := repo.ListVisibleProjects(t.Context(), "manager", "", 5) if err != nil || len(page.Items) == 0 || page.Items[0].CanManageMembers || page.Items[0].CanManageProject || page.Items[0].CanDeleteProject || page.CanCreateProject { t.Fatalf("撤销管理角色未生效:%+v,%v", page, err) } if err := repo.RevokeProjectRole(t.Context(), "root", "visible-00", "manager", domain.ProjectRoleReadOnly); err != nil { t.Fatal(err) } page, err = repo.ListVisibleProjects(t.Context(), "manager", "", 5) if err != nil || len(page.Items) == 0 || page.Items[0].ID == "visible-00" { t.Fatalf("已离开项目仍可见:%+v,%v", page, err) } for _, actor := range []string{"manager", "root"} { if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, "UPDATE users SET status='disabled' WHERE id=?"), actor); err != nil { t.Fatal(err) } if _, err := repo.ListVisibleProjects(t.Context(), actor, "", 5); !errors.Is(err, ErrProjectAccessDenied) { t.Fatalf("停用账户仍有权限:%v", err) } } for _, actor := range []string{"", "missing"} { if _, err := repo.ListVisibleProjects(t.Context(), actor, "", 5); !errors.Is(err, ErrProjectAccessDenied) { t.Fatalf("不存在身份没有被拒绝:%v", err) } } }) t.Run("invalid_limits_and_storage", func(t *testing.T) { for _, size := range []int{0, -1, 101} { if _, err := repo.ListVisibleProjects(t.Context(), "viewer", "", size); err == nil { t.Fatal("接受无效分页") } } if _, err := NewSQLProjectMemberRepository(nil).ListVisibleProjects(t.Context(), "viewer", "", 5); err == nil { t.Fatal("空数据库被视为成功") } }) }