package service_test import ( "context" "errors" "testing" "time" "github.com/wotra/wotra/internal/service" "github.com/wotra/wotra/internal/store" ) func newTestServices(t *testing.T) *service.EntryService { t.Helper() db, err := store.Open(":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) entryStore := store.NewEntryStore(db) closedDayStore := store.NewClosedDayStore(db) settingsStore := store.NewSettingsStore(db) tz, _ := time.LoadLocation("UTC") return service.NewEntryService(entryStore, closedDayStore, settingsStore, store.NewSyncStore(db), tz) } func TestStartStop(t *testing.T) { ctx := context.Background() svc := newTestServices(t) entry, err := svc.Start(ctx, "test entry") if err != nil { t.Fatalf("Start: %v", err) } if entry.ID == "" { t.Fatal("expected non-empty ID") } if entry.EndTime != nil { t.Fatal("expected running entry (nil EndTime)") } stopped, err := svc.Stop(ctx) if err != nil { t.Fatalf("Stop: %v", err) } if stopped.EndTime == nil { t.Fatal("expected entry to be stopped") } if *stopped.EndTime < stopped.StartTime { t.Fatal("end_time before start_time") } } func TestStartTwiceFails(t *testing.T) { ctx := context.Background() svc := newTestServices(t) if _, err := svc.Start(ctx, ""); err != nil { t.Fatal(err) } _, err := svc.Start(ctx, "") if err == nil { t.Fatal("expected error starting second entry") } if err != service.ErrEntryRunning { t.Fatalf("expected ErrEntryRunning, got %v", err) } } func TestStopWithoutStart(t *testing.T) { ctx := context.Background() svc := newTestServices(t) _, err := svc.Stop(ctx) if err != service.ErrEntryNotRunning { t.Fatalf("expected ErrEntryNotRunning, got %v", err) } } func TestUpdateEntry(t *testing.T) { ctx := context.Background() svc := newTestServices(t) entry, _ := svc.Start(ctx, "initial note") stopped, _ := svc.Stop(ctx) note := "updated note" updated, err := svc.Update(ctx, stopped.ID, service.UpdateEntryInput{Note: ¬e}) if err != nil { t.Fatalf("Update: %v", err) } if updated.Note != "updated note" { t.Errorf("expected 'updated note', got %q", updated.Note) } _ = entry } func TestDeleteEntry(t *testing.T) { ctx := context.Background() svc := newTestServices(t) entry, _ := svc.Start(ctx, "") svc.Stop(ctx) if err := svc.Delete(ctx, entry.ID); err != nil { t.Fatalf("Delete: %v", err) } entries, err := svc.List(ctx, "0000-01-01", "9999-12-31") if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("expected 0 entries after delete, got %d", len(entries)) } } func TestListEntries(t *testing.T) { ctx := context.Background() svc := newTestServices(t) for i := 0; i < 3; i++ { svc.Start(ctx, "") svc.Stop(ctx) } today := time.Now().UTC().Format("2006-01-02") entries, err := svc.List(ctx, today, today) if err != nil { t.Fatal(err) } if len(entries) != 3 { t.Fatalf("expected 3 entries, got %d", len(entries)) } } func TestCreateInterval(t *testing.T) { ctx := context.Background() svc := newTestServices(t) now := time.Now().UTC() startMs := now.Add(-2 * time.Hour).UnixMilli() endMs := now.Add(-1 * time.Hour).UnixMilli() entry, err := svc.CreateInterval(ctx, service.CreateIntervalInput{ StartTime: startMs, EndTime: endMs, Note: "manual entry", }) if err != nil { t.Fatalf("CreateInterval: %v", err) } if entry.EndTime == nil { t.Fatal("expected EndTime to be set") } if *entry.EndTime != endMs { t.Errorf("expected EndTime %d, got %d", endMs, *entry.EndTime) } if entry.Note != "manual entry" { t.Errorf("expected note 'manual entry', got %q", entry.Note) } } func TestCreateIntervalEndBeforeStart(t *testing.T) { ctx := context.Background() svc := newTestServices(t) now := time.Now().UTC().UnixMilli() _, err := svc.CreateInterval(ctx, service.CreateIntervalInput{ StartTime: now, EndTime: now - 1000, }) if err == nil { t.Fatal("expected error for end_time before start_time") } } func TestCreateIntervalCrossesMidnight(t *testing.T) { ctx := context.Background() svc := newTestServices(t) yesterday := time.Now().UTC().Add(-24 * time.Hour) startMs := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 0, 0, 0, time.UTC).UnixMilli() endMs := time.Now().UTC().Add(time.Hour).UnixMilli() _, err := svc.CreateInterval(ctx, service.CreateIntervalInput{ StartTime: startMs, EndTime: endMs, }) if err == nil { t.Fatal("expected ErrCrossesMidnight") } if !errors.Is(err, service.ErrCrossesMidnight) { t.Fatalf("expected ErrCrossesMidnight, got %v", err) } } func TestUpdateRejectsClosedDay(t *testing.T) { ctx := context.Background() db, err := store.Open(":memory:") if err != nil { t.Fatal(err) } defer db.Close() entryStore := store.NewEntryStore(db) closedDayStore := store.NewClosedDayStore(db) closedWeekStore := store.NewClosedWeekStore(db) settingsStore := store.NewSettingsStore(db) syncStore := store.NewSyncStore(db) tz, _ := time.LoadLocation("UTC") svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz) daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz) entry, _ := svc.Start(ctx, "") svc.Stop(ctx) today := time.Now().UTC().Format("2006-01-02") if _, err := daySvc.CloseDay(ctx, today); err != nil { t.Fatalf("CloseDay: %v", err) } note := "should fail" _, err = svc.Update(ctx, entry.ID, service.UpdateEntryInput{Note: ¬e}) if err == nil { t.Fatal("expected error updating entry in closed day") } if !errors.Is(err, service.ErrDayAlreadyClosed) { t.Fatalf("expected ErrDayAlreadyClosed, got %v", err) } } func TestCreateIntervalRejectsFutureDay(t *testing.T) { ctx := context.Background() svc := newTestServices(t) // Build a start_time that is tomorrow. tomorrow := time.Now().UTC().AddDate(0, 0, 1) startMs := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 9, 0, 0, 0, time.UTC).UnixMilli() endMs := startMs + 3_600_000 // +1h _, err := svc.CreateInterval(ctx, service.CreateIntervalInput{ StartTime: startMs, EndTime: endMs, Note: "future", }) if err == nil { t.Fatal("expected error creating interval in the future") } if !errors.Is(err, service.ErrFutureDay) { t.Fatalf("expected ErrFutureDay, got %v", err) } } func TestUpdateRejectsMoveToFutureDay(t *testing.T) { ctx := context.Background() svc := newTestServices(t) // Create a valid interval for today. now := time.Now().UTC() startMs := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC).UnixMilli() endMs := startMs + 3_600_000 entry, err := svc.CreateInterval(ctx, service.CreateIntervalInput{ StartTime: startMs, EndTime: endMs, }) if err != nil { t.Fatalf("CreateInterval: %v", err) } // Try to move start_time to tomorrow. tomorrow := now.AddDate(0, 0, 1) futureStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 8, 0, 0, 0, time.UTC).UnixMilli() _, err = svc.Update(ctx, entry.ID, service.UpdateEntryInput{ StartTime: &futureStart, }) if err == nil { t.Fatal("expected error moving entry to future day") } if !errors.Is(err, service.ErrFutureDay) { t.Fatalf("expected ErrFutureDay, got %v", err) } }