Files
code-utils/team_test.go
2026-08-15 17:18:00 +08:00

228 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// 团队协作本地单测:徽标计数、共享打标置脏、资料校验、历史节点与时间解析。
// 团队远端流程见 team_integration_test.go需要 TEST_API_URL
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)
}
}
}