Files
code-utils/team_test.go

228 lines
6.7 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
// 团队协作本地单测:徽标计数、共享打标置脏、资料校验、历史节点与时间解析。
// 团队远端流程见 team_integration_test.go需要本机 MySQL
import (
"path/filepath"
"strings"
"testing"
"time"
)
func newBadgeApp(t *testing.T) (*App, *Store) {
t.Helper()
s, e := OpenStore(filepath.Join(t.TempDir(), "team.db"))
if e != nil {
t.Fatal(e)
}
t.Cleanup(func() { s.db.Close() })
a := NewApp()
a.store = s
return a, s
}
func TestNavBadgesLocalCounts(t *testing.T) {
a, s := newBadgeApp(t)
b, e := a.GetNavBadges()
if e != nil {
t.Fatal(e)
}
if b.Unread != 0 || b.TodosOpen != 0 || b.TicketsActive != 0 || b.TodayDue != 0 || b.CalendarDot {
t.Fatalf("empty db should have zero badges: %+v", b)
}
today := time.Now().Format("2006-01-02")
if _, e := a.SaveTodo(Todo{Title: "今天到期", DueAt: today}); e != nil {
t.Fatal(e)
}
if _, e := a.SaveTodo(Todo{Title: "已逾期", DueAt: "2020-01-01"}); e != nil {
t.Fatal(e)
}
done, e := a.SaveTodo(Todo{Title: "已完成", DueAt: today})
if e != nil {
t.Fatal(e)
}
if e := s.SetTodoStatus(done.ID, "done"); e != nil {
t.Fatal(e)
}
pid, e := s.SaveProject(0, ProjectInput{Name: "BadgeProj", Path: t.TempDir()})
if e != nil {
t.Fatal(e)
}
if _, e := a.SaveTicket(Ticket{Title: "进行中的工单", ProjectID: pid.ID, StartAt: today, DueAt: today + "T23:00", Status: "in_progress"}); e != nil {
t.Fatal(e)
}
if _, e := s.AddMessage("todo_due", "提醒", "", "", 0); e != nil {
t.Fatal(e)
}
b, e = a.GetNavBadges()
if e != nil {
t.Fatal(e)
}
if b.Unread != 1 {
t.Fatalf("unread want 1, got %d", b.Unread)
}
if b.TodosOpen != 2 { // 完成的不算
t.Fatalf("todosOpen want 2, got %d", b.TodosOpen)
}
if b.TicketsActive != 1 {
t.Fatalf("ticketsActive want 1, got %d", b.TicketsActive)
}
if b.TodayDue != 3 { // 逾期 todo + 今天 todo + 今天工单
t.Fatalf("todayDue want 3, got %d", b.TodayDue)
}
if !b.CalendarDot {
t.Fatal("calendarDot should be on")
}
if b.TeamAssigned != 0 { // 未登录不读缓存
t.Fatalf("teamAssigned want 0 when logged out, got %d", b.TeamAssigned)
}
}
func TestSetItemTeamMarksDirty(t *testing.T) {
a, s := newBadgeApp(t)
todo, e := a.SaveTodo(Todo{Title: "可共享"})
if e != nil {
t.Fatal(e)
}
// 清脏位模拟已同步状态
if _, e := s.db.Exec(`UPDATE todos SET dirty=0`); e != nil {
t.Fatal(e)
}
var before string
_ = s.db.QueryRow(`SELECT updated_at FROM todos WHERE id=?`, todo.ID).Scan(&before)
if e := a.SetTodoTeam(todo.ID, 7); e != nil {
t.Fatal(e)
}
var teamID, dirty int64
var after string
if e := s.db.QueryRow(`SELECT team_id,dirty,updated_at FROM todos WHERE id=?`, todo.ID).Scan(&teamID, &dirty, &after); e != nil {
t.Fatal(e)
}
if teamID != 7 || dirty != 1 {
t.Fatalf("want team_id=7 dirty=1, got %d %d", teamID, dirty)
}
if after <= before {
t.Fatalf("updated_at should advance for LWW: %q -> %q", before, after)
}
// 列表返回 teamId取消共享归零
list, e := a.ListTodos("all", 0)
if e != nil || len(list) != 1 || list[0].TeamID != 7 {
t.Fatalf("ListTodos should expose teamId=7: %v %+v", e, list)
}
if e := a.SetTodoTeam(todo.ID, 0); e != nil {
t.Fatal(e)
}
if e := a.SetTodoTeam(9999, 1); e == nil || e.Error() != "NOT_FOUND" {
t.Fatalf("missing todo should NOT_FOUND, got %v", e)
}
}
func TestSanitizeProfile(t *testing.T) {
p := UserProfile{
Nickname: " 年糕崽崽 ",
Title: strings.Repeat("长", 60),
Email: "a@b.co",
Bio: " hi ",
TechTags: []string{" Go ", "go", "Vue", "", strings.Repeat("x", 40)},
}
if e := sanitizeProfile(&p); e != nil {
t.Fatal(e)
}
if p.Nickname != "年糕崽崽" || len([]rune(p.Title)) != 48 || p.Bio != "hi" {
t.Fatalf("trim/clamp failed: %+v", p)
}
// 标签去重(大小写不敏感)+ 截断
if len(p.TechTags) != 3 || p.TechTags[0] != "Go" || p.TechTags[1] != "Vue" || len(p.TechTags[2]) != 24 {
t.Fatalf("tags dedupe/clip failed: %v", p.TechTags)
}
bad := UserProfile{Email: "not-an-email"}
if e := sanitizeProfile(&bad); e == nil || e.Error() != "PROFILE_EMAIL_INVALID" {
t.Fatalf("want PROFILE_EMAIL_INVALID, got %v", e)
}
}
func TestProfileLocalRoundtrip(t *testing.T) {
a, _ := newBadgeApp(t)
saved, e := a.SaveMyProfile(UserProfile{Nickname: "小李", TechTags: []string{"Go", "Vue"}})
if e != nil {
t.Fatal(e)
}
if saved.UpdatedAt == "" {
t.Fatal("save should stamp updatedAt")
}
got, e := a.GetMyProfile()
if e != nil || got.Nickname != "小李" || len(got.TechTags) != 2 {
t.Fatalf("roundtrip failed: %v %+v", e, got)
}
if parsed := parseTechTags(`["a","b"]`); len(parsed) != 2 {
t.Fatalf("parseTechTags: %v", parsed)
}
if parsed := parseTechTags(`{bad json`); len(parsed) != 0 {
t.Fatalf("bad json should yield empty: %v", parsed)
}
}
func TestAppendHistoryNodeAndClock(t *testing.T) {
h := appendHistoryNode("", "open", "2026-08-13T01:00:00Z")
h = appendHistoryNode(h, "doing", "2026-08-13T02:00:00Z")
if !strings.HasPrefix(h, `[{"status":"open"`) || !strings.Contains(h, `"doing"`) || !strings.HasSuffix(h, "]") {
t.Fatalf("history chain broken: %s", h)
}
if got := appendHistoryNode("garbage", "done", "2026-08-13T03:00:00Z"); !strings.HasPrefix(got, `[{"status":"done"`) {
t.Fatalf("garbage history should reset: %s", got)
}
for _, ok := range []string{"00:00", "21:00", "23:59"} {
if _, valid := parseClockOK(ok); !valid {
t.Fatalf("%s should be valid", ok)
}
}
for _, bad := range []string{"", "24:00", "9:70", "abc", "12"} {
if _, valid := parseClockOK(bad); valid {
t.Fatalf("%s should be invalid", bad)
}
}
}
func TestTeamRoleRank(t *testing.T) {
if !(teamRoleRank("owner") > teamRoleRank("admin") && teamRoleRank("admin") > teamRoleRank("member") && teamRoleRank("member") > teamRoleRank("")) {
t.Fatal("role ranking broken")
}
}
// TestMigrateFromV7AddsTeamID 模拟 v7 老库升级:删掉 team_id 列并回退版本号,
// 重开库后迁移必须补回该列(防止只加迁移块忘了升 schemaVersion
func TestMigrateFromV7AddsTeamID(t *testing.T) {
path := filepath.Join(t.TempDir(), "old.db")
s, e := OpenStore(path)
if e != nil {
t.Fatal(e)
}
for _, q := range []string{
`ALTER TABLE todos DROP COLUMN team_id`,
`ALTER TABLE tickets DROP COLUMN team_id`,
`DELETE FROM schema_migrations WHERE version>=8`,
`INSERT OR IGNORE INTO schema_migrations(version,applied_at) VALUES(7,'2026-01-01')`,
} {
if _, e := s.db.Exec(q); e != nil {
t.Fatal(e)
}
}
s.db.Close()
s2, e := OpenStore(path)
if e != nil {
t.Fatalf("reopen v7 db: %v", e)
}
defer s2.db.Close()
for _, table := range []string{"todos", "tickets"} {
ok, e := s2.columnExists(table, "team_id")
if e != nil || !ok {
t.Fatalf("%s.team_id should be restored by migration (err=%v)", table, e)
}
}
}