Files
wotra/internal/service/day_service_test.go
Andreas Schneider 47c7a97d47 fix: keep closed week snapshot in sync when days change
When a day is closed, re-closed, or reopened, DayService now
recomputes worked_ms and delta_ms on the closed week containing
that day (if the week is already closed). This prevents stale
delta values after editing entries and re-closing a day.

- DayService.recomputeWeek: sums worked_ms from all closed_days
  in the week, updates closed_weeks row preserving expected_ms
- NewDayService now takes ClosedWeekStore
- WeekKeyForDayKey exported helper (used by DayService)
- TestWeekSnapshotUpdatesWhenDayReopened regression test
2026-04-30 18:16:22 +02:00

164 lines
4.1 KiB
Go

package service_test
import (
"context"
"testing"
"time"
"github.com/wotra/wotra/internal/domain"
"github.com/wotra/wotra/internal/service"
"github.com/wotra/wotra/internal/store"
)
func newTestDayServices(t *testing.T) (*service.EntryService, *service.DayService, *service.SettingsService) {
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)
closedWeekStore := store.NewClosedWeekStore(db)
settingsStore := store.NewSettingsStore(db)
tz, _ := time.LoadLocation("UTC")
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, tz)
settingsSvc := service.NewSettingsService(settingsStore)
return entrySvc, daySvc, settingsSvc
}
func TestCloseDayBasic(t *testing.T) {
ctx := context.Background()
entrySvc, daySvc, _ := newTestDayServices(t)
// Start and stop an entry
_, err := entrySvc.Start(ctx, "work")
if err != nil {
t.Fatal(err)
}
_, err = entrySvc.Stop(ctx)
if err != nil {
t.Fatal(err)
}
today := time.Now().UTC().Format("2006-01-02")
cd, err := daySvc.CloseDay(ctx, today)
if err != nil {
t.Fatalf("CloseDay: %v", err)
}
if cd.Kind != domain.DayKindWork {
t.Errorf("expected kind=work, got %s", cd.Kind)
}
if cd.WorkedMs < 0 {
t.Error("expected non-negative worked_ms")
}
}
func TestCloseDayWithRunningEntryFails(t *testing.T) {
ctx := context.Background()
entrySvc, daySvc, _ := newTestDayServices(t)
_, err := entrySvc.Start(ctx, "")
if err != nil {
t.Fatal(err)
}
today := time.Now().UTC().Format("2006-01-02")
_, err = daySvc.CloseDay(ctx, today)
if err == nil {
t.Fatal("expected error closing day with running entry")
}
if err != service.ErrRunningEntryOnDay {
t.Fatalf("expected ErrRunningEntryOnDay, got %v", err)
}
}
func TestCloseDayTwiceFails(t *testing.T) {
ctx := context.Background()
entrySvc, daySvc, _ := newTestDayServices(t)
entrySvc.Start(ctx, "")
entrySvc.Stop(ctx)
today := time.Now().UTC().Format("2006-01-02")
daySvc.CloseDay(ctx, today)
_, err := daySvc.CloseDay(ctx, today)
if err != service.ErrDayAlreadyClosed {
t.Fatalf("expected ErrDayAlreadyClosed, got %v", err)
}
}
func TestMarkDayHoliday(t *testing.T) {
ctx := context.Background()
_, daySvc, _ := newTestDayServices(t)
today := time.Now().UTC().Format("2006-01-02")
cd, err := daySvc.MarkDay(ctx, today, domain.DayKindHoliday)
if err != nil {
t.Fatalf("MarkDay: %v", err)
}
if cd.Kind != domain.DayKindHoliday {
t.Errorf("expected kind=holiday, got %s", cd.Kind)
}
// Monday-Friday = 40h/5 = 8h = 28800000ms expected
if today == time.Now().UTC().Format("2006-01-02") {
wd := int(time.Now().UTC().Weekday())
// workdays Mon-Fri (mask=31): weekdays 1-5
if wd >= 1 && wd <= 5 {
if cd.WorkedMs != 8*3600*1000 {
t.Errorf("expected 8h worked_ms for holiday on workday, got %d", cd.WorkedMs)
}
}
}
}
func TestReopenDay(t *testing.T) {
ctx := context.Background()
entrySvc, daySvc, _ := newTestDayServices(t)
entrySvc.Start(ctx, "")
entrySvc.Stop(ctx)
today := time.Now().UTC().Format("2006-01-02")
daySvc.CloseDay(ctx, today)
if err := daySvc.ReopenDay(ctx, today); err != nil {
t.Fatalf("ReopenDay: %v", err)
}
// Should be closeable again
_, err := daySvc.CloseDay(ctx, today)
if err != nil {
t.Fatalf("CloseDay after reopen: %v", err)
}
}
func TestSettingsUpsertAndHistory(t *testing.T) {
ctx := context.Background()
_, _, settingsSvc := newTestDayServices(t)
set, err := settingsSvc.Upsert(ctx, service.UpsertSettingsInput{
EffectiveFrom: "2024-01-01",
HoursPerWeek: 38.5,
WorkdaysMask: 31,
Timezone: "Europe/Berlin",
})
if err != nil {
t.Fatalf("Upsert: %v", err)
}
if set.HoursPerWeek != 38.5 {
t.Errorf("expected 38.5 h/week, got %f", set.HoursPerWeek)
}
history, err := settingsSvc.History(ctx)
if err != nil {
t.Fatal(err)
}
// Seeded default + our new one
if len(history) < 2 {
t.Fatalf("expected >=2 history entries, got %d", len(history))
}
}