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
234 lines
5.5 KiB
Go
234 lines
5.5 KiB
Go
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, 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)
|
|
tz, _ := time.LoadLocation("UTC")
|
|
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
|
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, 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)
|
|
}
|
|
}
|