package repo import ( "os" "testing" "github.com/pressly/goose/v3" ) func TestMain(m *testing.M) { goose.SetLogger(goose.NopLogger()) os.Exit(m.Run()) } func newTestStore(t *testing.T) *Store { t.Helper() s, err := NewStore("file::memory:?cache=shared") if err != nil { t.Fatalf("NewStore() failed: %v", err) } return s } func TestGetOrCreateUser(t *testing.T) { s := newTestStore(t) defer s.Close() u, err := s.GetOrCreateUser(12345) if err != nil { t.Fatalf("GetOrCreateUser() failed: %v", err) } if u.ID == 0 { t.Fatal("GetOrCreateUser() returned zero ID") } if u.ChatID != 12345 { t.Errorf("ChatID = %d, want 12345", u.ChatID) } if u.Timezone == "" { t.Errorf("Timezone is empty") } if u.Calendar != "gregorian" { t.Errorf("Calendar = %q, want gregorian", u.Calendar) } u2, err := s.GetOrCreateUser(12345) if err != nil { t.Fatalf("GetOrCreateUser() second call failed: %v", err) } if u2.ID != u.ID { t.Errorf("second call returned different ID: %d vs %d", u2.ID, u.ID) } u3, err := s.GetOrCreateUser(67890) if err != nil { t.Fatalf("GetOrCreateUser() third call failed: %v", err) } if u3.ID == u.ID { t.Error("different chat_id should produce different user ID") } } func TestGetOrCreateSettings(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) st, err := s.GetOrCreateSettings(user.ID) if err != nil { t.Fatalf("GetOrCreateSettings() failed: %v", err) } if !st.ReportEnabled { t.Error("ReportEnabled should default to true") } if st.ExportAccent != "ocean" { t.Errorf("ExportAccent = %q, want ocean", st.ExportAccent) } st.ExportAccent = "beach" st.ReportEnabled = false if err := s.UpdateSettings(st); err != nil { t.Fatalf("UpdateSettings() failed: %v", err) } st2, err := s.GetOrCreateSettings(user.ID) if err != nil { t.Fatalf("GetOrCreateSettings() after update failed: %v", err) } if st2.ExportAccent != "beach" { t.Errorf("ExportAccent = %q, want beach", st2.ExportAccent) } if st2.ReportEnabled { t.Error("ReportEnabled should be false after update") } } func TestGetOrCreateDay(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) day, err := s.GetOrCreateDay(user.ID, "2026-06-25") if err != nil { t.Fatalf("GetOrCreateDay() failed: %v", err) } if day.UserID != user.ID { t.Errorf("UserID = %d, want %d", day.UserID, user.ID) } if day.Date != "2026-06-25" { t.Errorf("Date = %q, want 2026-06-25", day.Date) } if day.IsDayOff { t.Error("IsDayOff should default to false") } if day.MinBreakThreshold != 300 { t.Errorf("MinBreakThreshold = %d, want 300", day.MinBreakThreshold) } day2, err := s.GetOrCreateDay(user.ID, "2026-06-25") if err != nil { t.Fatalf("GetOrCreateDay() second call failed: %v", err) } if day2.ID != day.ID { t.Errorf("second call returned different day ID") } got, err := s.GetDay(user.ID, "2026-06-25") if err != nil { t.Fatalf("GetDay() failed: %v", err) } if got.ID != day.ID { t.Errorf("GetDay() returned different day ID") } _, err = s.GetDay(user.ID, "2026-06-26") if err == nil { t.Error("GetDay() should fail for non-existent day") } } func TestDayOff(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) if err := s.SetDayOff(user.ID, "2026-06-25", "sick"); err != nil { t.Fatalf("SetDayOff() failed: %v", err) } day, err := s.GetDay(user.ID, "2026-06-25") if err != nil { t.Fatalf("GetDay() after SetDayOff failed: %v", err) } if !day.IsDayOff { t.Error("IsDayOff should be true after SetDayOff") } if day.DayOffReason != "sick" { t.Errorf("DayOffReason = %q, want sick", day.DayOffReason) } if err := s.RemoveDayOff(user.ID, "2026-06-25"); err != nil { t.Fatalf("RemoveDayOff() failed: %v", err) } day, _ = s.GetDay(user.ID, "2026-06-25") if day.IsDayOff { t.Error("IsDayOff should be false after RemoveDayOff") } } func TestCreateAndQueryEvents(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) day, _ := s.GetOrCreateDay(user.ID, "2026-06-25") wt := int64(2) if err := s.CreateEvent(user.ID, day.ID, "in", &wt, 1000, "note1"); err != nil { t.Fatalf("CreateEvent() in failed: %v", err) } if err := s.CreateEvent(user.ID, day.ID, "out", nil, 2000, ""); err != nil { t.Fatalf("CreateEvent() out failed: %v", err) } events, err := s.EventsForDayByDate(user.ID, "2026-06-25") if err != nil { t.Fatalf("EventsForDayByDate() failed: %v", err) } if len(events) != 2 { t.Fatalf("got %d events, want 2", len(events)) } if events[0].EventType != "in" || events[0].OccurredAt != 1000 { t.Errorf("first event: type=%q, time=%d", events[0].EventType, events[0].OccurredAt) } if events[0].WorkTypeID == nil || *events[0].WorkTypeID != 2 { t.Error("first event WorkTypeID should be 2") } if events[0].Note != "note1" { t.Errorf("first event Note = %q, want note1", events[0].Note) } if events[1].EventType != "out" || events[1].OccurredAt != 2000 { t.Errorf("second event: type=%q, time=%d", events[1].EventType, events[1].OccurredAt) } if events[1].WorkTypeID != nil { t.Error("out event WorkTypeID should be nil") } last, err := s.GetLastEvent(user.ID) if err != nil { t.Fatalf("GetLastEvent() failed: %v", err) } if last.EventType != "out" || last.OccurredAt != 2000 { t.Errorf("last event: type=%q, time=%d", last.EventType, last.OccurredAt) } } func TestCRUDEvent(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) day, _ := s.GetOrCreateDay(user.ID, "2026-06-25") wt := int64(1) s.CreateEvent(user.ID, day.ID, "in", &wt, 100, "") events, _ := s.EventsForDayByDate(user.ID, "2026-06-25") eid := events[0].ID if err := s.UpdateEventTime(eid, 150); err != nil { t.Fatalf("UpdateEventTime() failed: %v", err) } if err := s.UpdateEventWorkType(eid, 2); err != nil { t.Fatalf("UpdateEventWorkType() failed: %v", err) } if err := s.SetEventNote(eid, "updated"); err != nil { t.Fatalf("SetEventNote() failed: %v", err) } e, err := s.GetEventByID(eid, user.ID) if err != nil { t.Fatalf("GetEventByID() failed: %v", err) } if e.OccurredAt != 150 { t.Errorf("OccurredAt = %d, want 150", e.OccurredAt) } if e.WorkTypeID == nil || *e.WorkTypeID != 2 { t.Error("WorkTypeID should be 2") } if e.Note != "updated" { t.Errorf("Note = %q, want updated", e.Note) } if err := s.DeleteEvent(eid); err != nil { t.Fatalf("DeleteEvent() failed: %v", err) } events, _ = s.EventsForDayByDate(user.ID, "2026-06-25") if len(events) != 0 { t.Errorf("after delete: got %d events, want 0", len(events)) } } func TestRPGStats(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) stats, err := s.GetOrCreateRPGStats(user.ID) if err != nil { t.Fatalf("GetOrCreateRPGStats() failed: %v", err) } if stats.Level != 1 { t.Errorf("Level = %d, want 1", stats.Level) } if stats.XP != 0 { t.Errorf("XP = %d, want 0", stats.XP) } stats.XP = 5000 stats.Level = 10 stats.CurrentStreak = 5 stats.LongestStreak = 10 stats.TotalWorkSeconds = 36000 if err := s.UpdateRPGStats(stats); err != nil { t.Fatalf("UpdateRPGStats() failed: %v", err) } stats2, _ := s.GetOrCreateRPGStats(user.ID) if stats2.XP != 5000 || stats2.Level != 10 { t.Errorf("after update: XP=%d Level=%d, want 5000 10", stats2.XP, stats2.Level) } if stats2.CurrentStreak != 5 || stats2.LongestStreak != 10 { t.Errorf("after update: CurrentStreak=%d LongestStreak=%d, want 5 10", stats2.CurrentStreak, stats2.LongestStreak) } } func TestDailyXP(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) if err := s.UpsertDailyXP(user.ID, "2026-06-25", 100, 3600); err != nil { t.Fatalf("UpsertDailyXP() failed: %v", err) } if err := s.UpsertDailyXP(user.ID, "2026-06-26", 200, 7200); err != nil { t.Fatalf("UpsertDailyXP() second call failed: %v", err) } if err := s.UpsertDailyXP(user.ID, "2026-06-25", 50, 1800); err != nil { t.Fatalf("UpsertDailyXP() third call failed: %v", err) } total, err := s.SumDailyWorkSeconds(user.ID) if err != nil { t.Fatalf("SumDailyWorkSeconds() failed: %v", err) } if total != 12600 { t.Errorf("SumDailyWorkSeconds = %d, want 12600 (3600+7200+1800)", total) } records, err := s.GetDailyXPInRange(user.ID, "2026-06-25", "2026-06-26") if err != nil { t.Fatalf("GetDailyXPInRange() failed: %v", err) } if len(records) != 2 { t.Fatalf("got %d records, want 2", len(records)) } if records[0].Date != "2026-06-25" || records[0].WorkSeconds != 5400 { t.Errorf("record[0]: date=%q work=%d, want 2026-06-25 5400", records[0].Date, records[0].WorkSeconds) } if records[1].Date != "2026-06-26" || records[1].WorkSeconds != 7200 { t.Errorf("record[1]: date=%q work=%d, want 2026-06-26 7200", records[1].Date, records[1].WorkSeconds) } s.SetDailyWorkSeconds(user.ID, "2026-06-25", 3000) records, _ = s.GetDailyXPInRange(user.ID, "2026-06-25", "2026-06-25") if len(records) != 1 || records[0].WorkSeconds != 3000 { t.Errorf("after SetDailyWorkSeconds: work=%d, want 3000", records[0].WorkSeconds) } } func TestAchievements(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) a, err := s.GetAchievementByCode("first_clockin") if err != nil { t.Fatalf("GetAchievementByCode() failed: %v", err) } if a.Code != "first_clockin" || a.Name == "" { t.Errorf("achievement: code=%q name=%q", a.Code, a.Name) } has, err := s.HasAchievement(user.ID, "first_clockin") if err != nil { t.Fatalf("HasAchievement() failed: %v", err) } if has { t.Error("HasAchievement should be false before unlocking") } ok, err := s.UnlockAchievement(user.ID, a.ID) if err != nil { t.Fatalf("UnlockAchievement() failed: %v", err) } if !ok { t.Error("UnlockAchievement should return true on first unlock") } ok, err = s.UnlockAchievement(user.ID, a.ID) if err != nil { t.Fatalf("UnlockAchievement() duplicate failed: %v", err) } if ok { t.Error("UnlockAchievement should return false on duplicate") } has, _ = s.HasAchievement(user.ID, "first_clockin") if !has { t.Error("HasAchievement should be true after unlocking") } achs, err := s.GetUserAchievements(user.ID) if err != nil { t.Fatalf("GetUserAchievements() failed: %v", err) } if len(achs) != 1 { t.Fatalf("got %d achievements, want 1", len(achs)) } if achs[0].Code != "first_clockin" { t.Errorf("achievement code = %q, want first_clockin", achs[0].Code) } } func TestGetWorkTypes(t *testing.T) { s := newTestStore(t) defer s.Close() wts, err := s.GetWorkTypes() if err != nil { t.Fatalf("GetWorkTypes() failed: %v", err) } if len(wts) != 2 { t.Fatalf("got %d work types, want 2", len(wts)) } wt, err := s.GetWorkType(1) if err != nil { t.Fatalf("GetWorkType(1) failed: %v", err) } if wt.Name != "onsite" { t.Errorf("WorkType 1 = %q, want onsite", wt.Name) } _, err = s.GetWorkType(999) if err == nil { t.Error("GetWorkType(999) should fail") } } func TestGetUserDaysInRange(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) s.GetOrCreateDay(user.ID, "2026-06-01") s.GetOrCreateDay(user.ID, "2026-06-02") s.GetOrCreateDay(user.ID, "2026-06-05") days, err := s.GetUserDaysInRange(user.ID, "2026-06-01", "2026-06-10") if err != nil { t.Fatalf("GetUserDaysInRange() failed: %v", err) } if len(days) != 3 { t.Fatalf("got %d days, want 3", len(days)) } days, _ = s.GetUserDaysInRange(user.ID, "2026-06-03", "2026-06-10") if len(days) != 1 { t.Fatalf("got %d days in narrow range, want 1", len(days)) } } func TestGetDatesWithEvents(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) d1, _ := s.GetOrCreateDay(user.ID, "2026-06-01") d2, _ := s.GetOrCreateDay(user.ID, "2026-06-02") s.GetOrCreateDay(user.ID, "2026-06-03") s.CreateEvent(user.ID, d1.ID, "in", nil, 100, "") s.CreateEvent(user.ID, d2.ID, "in", nil, 200, "") dates, err := s.GetDatesWithEvents(user.ID) if err != nil { t.Fatalf("GetDatesWithEvents() failed: %v", err) } if len(dates) != 2 { t.Fatalf("got %d dates, want 2", len(dates)) } distinct, err := s.GetDistinctEventDates(user.ID, "2026-06-01", "2026-06-30") if err != nil { t.Fatalf("GetDistinctEventDates() failed: %v", err) } if len(distinct) != 2 { t.Fatalf("got %d distinct dates, want 2", len(distinct)) } } func TestSumWorkSecondsByWorkType(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) day, _ := s.GetOrCreateDay(user.ID, "2026-06-25") wt2 := int64(2) s.CreateEvent(user.ID, day.ID, "in", &wt2, 100, "") s.CreateEvent(user.ID, day.ID, "out", nil, 3600, "") s.SetDailyWorkSeconds(user.ID, "2026-06-25", 3500) rem, ons, err := s.SumWorkSecondsByWorkType(user.ID) if err != nil { t.Fatalf("SumWorkSecondsByWorkType() failed: %v", err) } if rem+ons == 0 { t.Error("expected non-zero work seconds, got 0 for both types") } } func TestDeleteUserData(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) s.GetOrCreateSettings(user.ID) s.GetOrCreateRPGStats(user.ID) s.GetOrCreateDay(user.ID, "2026-06-25") s.UpsertDailyXP(user.ID, "2026-06-25", 100, 3600) a, _ := s.GetAchievementByCode("first_clockin") s.UnlockAchievement(user.ID, a.ID) if err := s.DeleteUserData(user.ID); err != nil { t.Fatalf("DeleteUserData() failed: %v", err) } user2, err := s.GetOrCreateUser(12345) if err != nil { t.Fatalf("GetOrCreateUser() after delete failed: %v", err) } if user2.ID <= user.ID { t.Error("new user should get a fresh ID") } total, _ := s.SumDailyWorkSeconds(user.ID) if total != 0 { t.Errorf("work seconds after delete = %d, want 0", total) } } func TestMarkReportSent(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) if err := s.MarkReportSent(user.ID, "2026-06-25"); err != nil { t.Fatalf("MarkReportSent() failed: %v", err) } settings, _ := s.GetOrCreateSettings(user.ID) if settings.LastReportDate != "2026-06-25" { t.Errorf("LastReportDate = %q, want 2026-06-25", settings.LastReportDate) } } func TestGetAllUsers(t *testing.T) { s := newTestStore(t) defer s.Close() s.GetOrCreateUser(12345) s.GetOrCreateUser(67890) users, err := s.GetAllUsers() if err != nil { t.Fatalf("GetAllUsers() failed: %v", err) } if len(users) != 2 { t.Fatalf("got %d users, want 2", len(users)) } } func TestUpdateUserAndDay(t *testing.T) { s := newTestStore(t) defer s.Close() user, _ := s.GetOrCreateUser(12345) user.Timezone = "America/New_York" user.Username = "testuser" if err := s.UpdateUser(user); err != nil { t.Fatalf("UpdateUser() failed: %v", err) } user2, _ := s.GetOrCreateUser(12345) if user2.Timezone != "America/New_York" { t.Errorf("Timezone = %q, want America/New_York", user2.Timezone) } if user2.Username != "testuser" { t.Errorf("Username = %q, want testuser", user2.Username) } day, _ := s.GetOrCreateDay(user.ID, "2026-06-25") day.MinBreakThreshold = 600 if err := s.UpdateDay(day); err != nil { t.Fatalf("UpdateDay() failed: %v", err) } day2, _ := s.GetDay(user.ID, "2026-06-25") if day2.MinBreakThreshold != 600 { t.Errorf("MinBreakThreshold = %d, want 600", day2.MinBreakThreshold) } }