feat: add /setcalendar command, store tests, and improve error handling
All checks were successful
CI / build (push) Successful in 48s
All checks were successful
CI / build (push) Successful in 48s
- Add /setcalendar command for calendar type parity (Settings UI) - Add 17 store tests covering all repository methods - Add handler tests for formatDuration, fmtHHMM, fmtDDHHMM - Add domain tests for BurnoutLevel.String, NavMonth, FormatSalary - Fix silent error discarding in save* helpers (log write failures) - Fix computeAndAwardXP error handling instead of discarding - Wrap critical store.go SQL errors with fmt.Errorf context - Update README commands table with /setcalendar
This commit is contained in:
@@ -21,16 +21,16 @@ type Store struct {
|
||||
func NewStore(path string) (*Store, error) {
|
||||
db, err := sql.Open(driverName, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("ping: %w", err)
|
||||
}
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
slog.Warn("failed to set WAL mode", "error", err)
|
||||
}
|
||||
if err := runMigrations(db); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
return &Store{db: db}, nil
|
||||
}
|
||||
@@ -38,33 +38,36 @@ func NewStore(path string) (*Store, error) {
|
||||
func (s *Store) DeleteUserData(userID int64) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM events WHERE day_id IN (SELECT id FROM days WHERE user_id=?)`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data events: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM days WHERE user_id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data days: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM user_achievements WHERE user_id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data achievements: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM daily_xp WHERE user_id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data daily_xp: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM user_rpg WHERE user_id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data rpg: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM user_settings WHERE user_id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data settings: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM users WHERE id=?`, userID); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("delete user data users: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("delete user data commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
@@ -98,7 +101,7 @@ func (s *Store) GetOrCreateUser(chatID int64) (*domain.User, error) {
|
||||
"INSERT OR IGNORE INTO users (chat_id, username) VALUES (?, ?)",
|
||||
chatID, randomUsername(),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("get or create user: %w", err)
|
||||
}
|
||||
return s.GetUserByChatID(chatID)
|
||||
}
|
||||
@@ -113,7 +116,7 @@ func (s *Store) GetUserByChatID(chatID int64) (*domain.User, error) {
|
||||
&u.ID, &u.ChatID, &u.Username, &u.Timezone, &u.IsActive,
|
||||
&u.Calendar, &u.CreatedAt, &u.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("get user by chat id: %w", err)
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
@@ -123,7 +126,10 @@ func (s *Store) UpdateUser(user *domain.User) error {
|
||||
"UPDATE users SET username=?, timezone=?, is_active=?, calendar=?, updated_at=unixepoch() WHERE id=?",
|
||||
user.Username, user.Timezone, user.IsActive, user.Calendar, user.ID,
|
||||
)
|
||||
return err
|
||||
if err != nil {
|
||||
return fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetAllUsers() ([]domain.User, error) {
|
||||
|
||||
593
internal/repo/store_test.go
Normal file
593
internal/repo/store_test.go
Normal file
@@ -0,0 +1,593 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user