refactor: introduce clock to make tests reproducible
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
assets "github.com/wotra/wotra"
|
assets "github.com/wotra/wotra"
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/config"
|
"github.com/wotra/wotra/internal/config"
|
||||||
"github.com/wotra/wotra/internal/handler"
|
"github.com/wotra/wotra/internal/handler"
|
||||||
"github.com/wotra/wotra/internal/service"
|
"github.com/wotra/wotra/internal/service"
|
||||||
@@ -46,10 +47,12 @@ func main() {
|
|||||||
settingsStore := store.NewSettingsStore(db)
|
settingsStore := store.NewSettingsStore(db)
|
||||||
syncStore := store.NewSyncStore(db)
|
syncStore := store.NewSyncStore(db)
|
||||||
|
|
||||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
clk := clock.Real()
|
||||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
|
||||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz, clk)
|
||||||
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz)
|
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz, clk)
|
||||||
|
settingsSvc := service.NewSettingsService(settingsStore, syncStore, clk)
|
||||||
|
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz, clk)
|
||||||
|
|
||||||
// Background goroutine: auto-stop entries that cross midnight
|
// Background goroutine: auto-stop entries that cross midnight
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
28
internal/clock/clock.go
Normal file
28
internal/clock/clock.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Clock is the time source injected into services.
|
||||||
|
// Production code uses Real(); tests use Fixed().
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real returns a Clock backed by time.Now().
|
||||||
|
func Real() Clock { return realClock{} }
|
||||||
|
|
||||||
|
type realClock struct{}
|
||||||
|
|
||||||
|
func (realClock) Now() time.Time { return time.Now() }
|
||||||
|
|
||||||
|
// FixedClock always returns T. Advance shifts T forward.
|
||||||
|
// Intended for tests only.
|
||||||
|
type FixedClock struct{ T time.Time }
|
||||||
|
|
||||||
|
// Fixed returns a *FixedClock set to t.
|
||||||
|
func Fixed(t time.Time) *FixedClock { return &FixedClock{T: t} }
|
||||||
|
|
||||||
|
func (f *FixedClock) Now() time.Time { return f.T }
|
||||||
|
|
||||||
|
// Advance shifts the clock forward by d.
|
||||||
|
func (f *FixedClock) Advance(d time.Duration) { f.T = f.T.Add(d) }
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,7 @@ type DayService struct {
|
|||||||
settings *store.SettingsStore
|
settings *store.SettingsStore
|
||||||
syncStore *store.SyncStore
|
syncStore *store.SyncStore
|
||||||
tz *time.Location
|
tz *time.Location
|
||||||
|
clock clock.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDayService(
|
func NewDayService(
|
||||||
@@ -33,6 +35,7 @@ func NewDayService(
|
|||||||
settings *store.SettingsStore,
|
settings *store.SettingsStore,
|
||||||
syncStore *store.SyncStore,
|
syncStore *store.SyncStore,
|
||||||
tz *time.Location,
|
tz *time.Location,
|
||||||
|
clk clock.Clock,
|
||||||
) *DayService {
|
) *DayService {
|
||||||
return &DayService{
|
return &DayService{
|
||||||
entries: entries,
|
entries: entries,
|
||||||
@@ -41,6 +44,7 @@ func NewDayService(
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
syncStore: syncStore,
|
syncStore: syncStore,
|
||||||
tz: tz,
|
tz: tz,
|
||||||
|
clock: clk,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ func (s *DayService) recomputeWeek(ctx context.Context, dayKey string) error {
|
|||||||
}
|
}
|
||||||
cw.WorkedMs = totalWorkedMs
|
cw.WorkedMs = totalWorkedMs
|
||||||
cw.DeltaMs = totalWorkedMs - cw.ExpectedMs
|
cw.DeltaMs = totalWorkedMs - cw.ExpectedMs
|
||||||
cw.UpdatedAt = time.Now().UnixMilli()
|
cw.UpdatedAt = s.clock.Now().UnixMilli()
|
||||||
if err := s.closedWeeks.Upsert(ctx, cw); err != nil {
|
if err := s.closedWeeks.Upsert(ctx, cw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -130,7 +134,7 @@ func (s *DayService) CloseDay(ctx context.Context, dayKey string) (*domain.Close
|
|||||||
totalMs += e.DurationMs()
|
totalMs += e.DurationMs()
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := s.clock.Now().UnixMilli()
|
||||||
cd := &domain.ClosedDay{
|
cd := &domain.ClosedDay{
|
||||||
DayKey: dayKey,
|
DayKey: dayKey,
|
||||||
StartTime: &minStart,
|
StartTime: &minStart,
|
||||||
@@ -172,7 +176,7 @@ func (s *DayService) MarkDay(ctx context.Context, dayKey string, kind domain.Day
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := s.clock.Now().UnixMilli()
|
||||||
cd := &domain.ClosedDay{
|
cd := &domain.ClosedDay{
|
||||||
DayKey: dayKey,
|
DayKey: dayKey,
|
||||||
WorkedMs: workedMs,
|
WorkedMs: workedMs,
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/service"
|
"github.com/wotra/wotra/internal/service"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestDayServices(t *testing.T) (*service.EntryService, *service.DayService, *service.SettingsService) {
|
// dayTestAnchor is a fixed Tuesday used as "now" across day service tests.
|
||||||
|
var dayTestAnchor = time.Date(2026, 5, 13, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func newTestDayServices(t *testing.T) (*service.EntryService, *service.DayService, *service.SettingsService, *clock.FixedClock) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := store.Open(":memory:")
|
db, err := store.Open(":memory:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -24,18 +28,18 @@ func newTestDayServices(t *testing.T) (*service.EntryService, *service.DayServic
|
|||||||
settingsStore := store.NewSettingsStore(db)
|
settingsStore := store.NewSettingsStore(db)
|
||||||
syncStore := store.NewSyncStore(db)
|
syncStore := store.NewSyncStore(db)
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
|
clk := clock.Fixed(dayTestAnchor)
|
||||||
|
|
||||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz, clk)
|
||||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz, clk)
|
||||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
settingsSvc := service.NewSettingsService(settingsStore, syncStore, clk)
|
||||||
return entrySvc, daySvc, settingsSvc
|
return entrySvc, daySvc, settingsSvc, clk
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloseDayBasic(t *testing.T) {
|
func TestCloseDayBasic(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, _ := newTestDayServices(t)
|
entrySvc, daySvc, _, clk := newTestDayServices(t)
|
||||||
|
|
||||||
// Start and stop an entry
|
|
||||||
_, err := entrySvc.Start(ctx, "work")
|
_, err := entrySvc.Start(ctx, "work")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -45,7 +49,7 @@ func TestCloseDayBasic(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
cd, err := daySvc.CloseDay(ctx, today)
|
cd, err := daySvc.CloseDay(ctx, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CloseDay: %v", err)
|
t.Fatalf("CloseDay: %v", err)
|
||||||
@@ -60,14 +64,14 @@ func TestCloseDayBasic(t *testing.T) {
|
|||||||
|
|
||||||
func TestCloseDayWithRunningEntryFails(t *testing.T) {
|
func TestCloseDayWithRunningEntryFails(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, _ := newTestDayServices(t)
|
entrySvc, daySvc, _, clk := newTestDayServices(t)
|
||||||
|
|
||||||
_, err := entrySvc.Start(ctx, "")
|
_, err := entrySvc.Start(ctx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
_, err = daySvc.CloseDay(ctx, today)
|
_, err = daySvc.CloseDay(ctx, today)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error closing day with running entry")
|
t.Fatal("expected error closing day with running entry")
|
||||||
@@ -79,11 +83,11 @@ func TestCloseDayWithRunningEntryFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestCloseDayTwiceFails(t *testing.T) {
|
func TestCloseDayTwiceFails(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, _ := newTestDayServices(t)
|
entrySvc, daySvc, _, clk := newTestDayServices(t)
|
||||||
|
|
||||||
entrySvc.Start(ctx, "")
|
entrySvc.Start(ctx, "")
|
||||||
entrySvc.Stop(ctx)
|
entrySvc.Stop(ctx)
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
daySvc.CloseDay(ctx, today)
|
daySvc.CloseDay(ctx, today)
|
||||||
|
|
||||||
_, err := daySvc.CloseDay(ctx, today)
|
_, err := daySvc.CloseDay(ctx, today)
|
||||||
@@ -94,9 +98,9 @@ func TestCloseDayTwiceFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestMarkDayHoliday(t *testing.T) {
|
func TestMarkDayHoliday(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, daySvc, _ := newTestDayServices(t)
|
_, daySvc, _, clk := newTestDayServices(t)
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
cd, err := daySvc.MarkDay(ctx, today, domain.DayKindHoliday)
|
cd, err := daySvc.MarkDay(ctx, today, domain.DayKindHoliday)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MarkDay: %v", err)
|
t.Fatalf("MarkDay: %v", err)
|
||||||
@@ -104,32 +108,28 @@ func TestMarkDayHoliday(t *testing.T) {
|
|||||||
if cd.Kind != domain.DayKindHoliday {
|
if cd.Kind != domain.DayKindHoliday {
|
||||||
t.Errorf("expected kind=holiday, got %s", cd.Kind)
|
t.Errorf("expected kind=holiday, got %s", cd.Kind)
|
||||||
}
|
}
|
||||||
// Monday-Friday = 40h/5 = 8h = 28800000ms expected
|
// dayTestAnchor is a Tuesday (weekday 2): Mon-Fri workdays → 40h/5 = 8h = 28800000ms
|
||||||
if today == time.Now().UTC().Format("2006-01-02") {
|
wd := int(clk.Now().UTC().Weekday())
|
||||||
wd := int(time.Now().UTC().Weekday())
|
|
||||||
// workdays Mon-Fri (mask=31): weekdays 1-5
|
|
||||||
if wd >= 1 && wd <= 5 {
|
if wd >= 1 && wd <= 5 {
|
||||||
if cd.WorkedMs != 8*3600*1000 {
|
if cd.WorkedMs != 8*3600*1000 {
|
||||||
t.Errorf("expected 8h worked_ms for holiday on workday, got %d", cd.WorkedMs)
|
t.Errorf("expected 8h worked_ms for holiday on workday, got %d", cd.WorkedMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReopenDay(t *testing.T) {
|
func TestReopenDay(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, _ := newTestDayServices(t)
|
entrySvc, daySvc, _, clk := newTestDayServices(t)
|
||||||
|
|
||||||
entrySvc.Start(ctx, "")
|
entrySvc.Start(ctx, "")
|
||||||
entrySvc.Stop(ctx)
|
entrySvc.Stop(ctx)
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
daySvc.CloseDay(ctx, today)
|
daySvc.CloseDay(ctx, today)
|
||||||
|
|
||||||
if err := daySvc.ReopenDay(ctx, today); err != nil {
|
if err := daySvc.ReopenDay(ctx, today); err != nil {
|
||||||
t.Fatalf("ReopenDay: %v", err)
|
t.Fatalf("ReopenDay: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be closeable again
|
|
||||||
_, err := daySvc.CloseDay(ctx, today)
|
_, err := daySvc.CloseDay(ctx, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CloseDay after reopen: %v", err)
|
t.Fatalf("CloseDay after reopen: %v", err)
|
||||||
@@ -138,7 +138,7 @@ func TestReopenDay(t *testing.T) {
|
|||||||
|
|
||||||
func TestSettingsUpsertAndHistory(t *testing.T) {
|
func TestSettingsUpsertAndHistory(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, _, settingsSvc := newTestDayServices(t)
|
_, _, settingsSvc, _ := newTestDayServices(t)
|
||||||
|
|
||||||
set, err := settingsSvc.Upsert(ctx, service.UpsertSettingsInput{
|
set, err := settingsSvc.Upsert(ctx, service.UpsertSettingsInput{
|
||||||
EffectiveFrom: "2024-01-01",
|
EffectiveFrom: "2024-01-01",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,7 @@ type EntryService struct {
|
|||||||
settings *store.SettingsStore
|
settings *store.SettingsStore
|
||||||
syncStore *store.SyncStore
|
syncStore *store.SyncStore
|
||||||
tz *time.Location
|
tz *time.Location
|
||||||
|
clock clock.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEntryService(
|
func NewEntryService(
|
||||||
@@ -39,6 +41,7 @@ func NewEntryService(
|
|||||||
settings *store.SettingsStore,
|
settings *store.SettingsStore,
|
||||||
syncStore *store.SyncStore,
|
syncStore *store.SyncStore,
|
||||||
tz *time.Location,
|
tz *time.Location,
|
||||||
|
clk clock.Clock,
|
||||||
) *EntryService {
|
) *EntryService {
|
||||||
return &EntryService{
|
return &EntryService{
|
||||||
entries: entries,
|
entries: entries,
|
||||||
@@ -46,11 +49,12 @@ func NewEntryService(
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
syncStore: syncStore,
|
syncStore: syncStore,
|
||||||
tz: tz,
|
tz: tz,
|
||||||
|
clock: clk,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EntryService) nowMs() int64 {
|
func (s *EntryService) nowMs() int64 {
|
||||||
return time.Now().UnixMilli()
|
return s.clock.Now().UnixMilli()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EntryService) dayKeyForMs(ms int64) string {
|
func (s *EntryService) dayKeyForMs(ms int64) string {
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/service"
|
"github.com/wotra/wotra/internal/service"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestServices(t *testing.T) *service.EntryService {
|
// testAnchor is a fixed Tuesday used as "now" across entry service tests.
|
||||||
|
var testAnchor = time.Date(2026, 5, 13, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func newTestServices(t *testing.T) (*service.EntryService, *clock.FixedClock) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := store.Open(":memory:")
|
db, err := store.Open(":memory:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,12 +27,14 @@ func newTestServices(t *testing.T) *service.EntryService {
|
|||||||
settingsStore := store.NewSettingsStore(db)
|
settingsStore := store.NewSettingsStore(db)
|
||||||
|
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
return service.NewEntryService(entryStore, closedDayStore, settingsStore, store.NewSyncStore(db), tz)
|
clk := clock.Fixed(testAnchor)
|
||||||
|
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, store.NewSyncStore(db), tz, clk)
|
||||||
|
return svc, clk
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStartStop(t *testing.T) {
|
func TestStartStop(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, _ := newTestServices(t)
|
||||||
|
|
||||||
entry, err := svc.Start(ctx, "test entry")
|
entry, err := svc.Start(ctx, "test entry")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,7 +61,7 @@ func TestStartStop(t *testing.T) {
|
|||||||
|
|
||||||
func TestStartTwiceFails(t *testing.T) {
|
func TestStartTwiceFails(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, _ := newTestServices(t)
|
||||||
|
|
||||||
if _, err := svc.Start(ctx, ""); err != nil {
|
if _, err := svc.Start(ctx, ""); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -71,7 +77,7 @@ func TestStartTwiceFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestStopWithoutStart(t *testing.T) {
|
func TestStopWithoutStart(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, _ := newTestServices(t)
|
||||||
|
|
||||||
_, err := svc.Stop(ctx)
|
_, err := svc.Stop(ctx)
|
||||||
if err != service.ErrEntryNotRunning {
|
if err != service.ErrEntryNotRunning {
|
||||||
@@ -81,7 +87,7 @@ func TestStopWithoutStart(t *testing.T) {
|
|||||||
|
|
||||||
func TestUpdateEntry(t *testing.T) {
|
func TestUpdateEntry(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, _ := newTestServices(t)
|
||||||
|
|
||||||
entry, _ := svc.Start(ctx, "initial note")
|
entry, _ := svc.Start(ctx, "initial note")
|
||||||
stopped, _ := svc.Stop(ctx)
|
stopped, _ := svc.Stop(ctx)
|
||||||
@@ -99,7 +105,7 @@ func TestUpdateEntry(t *testing.T) {
|
|||||||
|
|
||||||
func TestDeleteEntry(t *testing.T) {
|
func TestDeleteEntry(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, _ := newTestServices(t)
|
||||||
|
|
||||||
entry, _ := svc.Start(ctx, "")
|
entry, _ := svc.Start(ctx, "")
|
||||||
svc.Stop(ctx)
|
svc.Stop(ctx)
|
||||||
@@ -119,14 +125,14 @@ func TestDeleteEntry(t *testing.T) {
|
|||||||
|
|
||||||
func TestListEntries(t *testing.T) {
|
func TestListEntries(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
svc.Start(ctx, "")
|
svc.Start(ctx, "")
|
||||||
svc.Stop(ctx)
|
svc.Stop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
entries, err := svc.List(ctx, today, today)
|
entries, err := svc.List(ctx, today, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -138,9 +144,9 @@ func TestListEntries(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateInterval(t *testing.T) {
|
func TestCreateInterval(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := clk.Now().UTC()
|
||||||
startMs := now.Add(-2 * time.Hour).UnixMilli()
|
startMs := now.Add(-2 * time.Hour).UnixMilli()
|
||||||
endMs := now.Add(-1 * time.Hour).UnixMilli()
|
endMs := now.Add(-1 * time.Hour).UnixMilli()
|
||||||
|
|
||||||
@@ -165,9 +171,9 @@ func TestCreateInterval(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateIntervalEndBeforeStart(t *testing.T) {
|
func TestCreateIntervalEndBeforeStart(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
now := time.Now().UTC().UnixMilli()
|
now := clk.Now().UTC().UnixMilli()
|
||||||
_, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
_, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
||||||
StartTime: now,
|
StartTime: now,
|
||||||
EndTime: now - 1000,
|
EndTime: now - 1000,
|
||||||
@@ -179,11 +185,11 @@ func TestCreateIntervalEndBeforeStart(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateIntervalCrossesMidnight(t *testing.T) {
|
func TestCreateIntervalCrossesMidnight(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
yesterday := time.Now().UTC().Add(-24 * time.Hour)
|
yesterday := clk.Now().UTC().Add(-24 * time.Hour)
|
||||||
startMs := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 0, 0, 0, time.UTC).UnixMilli()
|
startMs := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 0, 0, 0, time.UTC).UnixMilli()
|
||||||
endMs := time.Now().UTC().Add(time.Hour).UnixMilli()
|
endMs := clk.Now().UTC().Add(time.Hour).UnixMilli()
|
||||||
|
|
||||||
_, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
_, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
||||||
StartTime: startMs,
|
StartTime: startMs,
|
||||||
@@ -212,13 +218,14 @@ func TestUpdateRejectsClosedDay(t *testing.T) {
|
|||||||
settingsStore := store.NewSettingsStore(db)
|
settingsStore := store.NewSettingsStore(db)
|
||||||
syncStore := store.NewSyncStore(db)
|
syncStore := store.NewSyncStore(db)
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
clk := clock.Fixed(testAnchor)
|
||||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz, clk)
|
||||||
|
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz, clk)
|
||||||
|
|
||||||
entry, _ := svc.Start(ctx, "")
|
entry, _ := svc.Start(ctx, "")
|
||||||
svc.Stop(ctx)
|
svc.Stop(ctx)
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := clk.Now().UTC().Format("2006-01-02")
|
||||||
if _, err := daySvc.CloseDay(ctx, today); err != nil {
|
if _, err := daySvc.CloseDay(ctx, today); err != nil {
|
||||||
t.Fatalf("CloseDay: %v", err)
|
t.Fatalf("CloseDay: %v", err)
|
||||||
}
|
}
|
||||||
@@ -235,10 +242,9 @@ func TestUpdateRejectsClosedDay(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateIntervalRejectsFutureDay(t *testing.T) {
|
func TestCreateIntervalRejectsFutureDay(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
// Build a start_time that is tomorrow.
|
tomorrow := clk.Now().UTC().AddDate(0, 0, 1)
|
||||||
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
|
|
||||||
startMs := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 9, 0, 0, 0, time.UTC).UnixMilli()
|
startMs := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 9, 0, 0, 0, time.UTC).UnixMilli()
|
||||||
endMs := startMs + 3_600_000 // +1h
|
endMs := startMs + 3_600_000 // +1h
|
||||||
|
|
||||||
@@ -257,10 +263,9 @@ func TestCreateIntervalRejectsFutureDay(t *testing.T) {
|
|||||||
|
|
||||||
func TestUpdateRejectsMoveToFutureDay(t *testing.T) {
|
func TestUpdateRejectsMoveToFutureDay(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
svc := newTestServices(t)
|
svc, clk := newTestServices(t)
|
||||||
|
|
||||||
// Create a valid interval for today.
|
now := clk.Now().UTC()
|
||||||
now := time.Now().UTC()
|
|
||||||
startMs := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC).UnixMilli()
|
startMs := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC).UnixMilli()
|
||||||
endMs := startMs + 3_600_000
|
endMs := startMs + 3_600_000
|
||||||
entry, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
entry, err := svc.CreateInterval(ctx, service.CreateIntervalInput{
|
||||||
@@ -271,7 +276,6 @@ func TestUpdateRejectsMoveToFutureDay(t *testing.T) {
|
|||||||
t.Fatalf("CreateInterval: %v", err)
|
t.Fatalf("CreateInterval: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to move start_time to tomorrow.
|
|
||||||
tomorrow := now.AddDate(0, 0, 1)
|
tomorrow := now.AddDate(0, 0, 1)
|
||||||
futureStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 8, 0, 0, 0, time.UTC).UnixMilli()
|
futureStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 8, 0, 0, 0, time.UTC).UnixMilli()
|
||||||
_, err = svc.Update(ctx, entry.ID, service.UpdateEntryInput{
|
_, err = svc.Update(ctx, entry.ID, service.UpdateEntryInput{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
@@ -24,15 +25,16 @@ var (
|
|||||||
type SettingsService struct {
|
type SettingsService struct {
|
||||||
store *store.SettingsStore
|
store *store.SettingsStore
|
||||||
syncStore *store.SyncStore
|
syncStore *store.SyncStore
|
||||||
|
clock clock.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettingsService(s *store.SettingsStore, syncStore *store.SyncStore) *SettingsService {
|
func NewSettingsService(s *store.SettingsStore, syncStore *store.SyncStore, clk clock.Clock) *SettingsService {
|
||||||
return &SettingsService{store: s, syncStore: syncStore}
|
return &SettingsService{store: s, syncStore: syncStore, clock: clk}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current returns settings effective as of today.
|
// Current returns settings effective as of today.
|
||||||
func (s *SettingsService) Current(ctx context.Context) (*domain.Settings, error) {
|
func (s *SettingsService) Current(ctx context.Context) (*domain.Settings, error) {
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := s.clock.Now().UTC().Format("2006-01-02")
|
||||||
set, err := s.store.Current(ctx, today)
|
set, err := s.store.Current(ctx, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrNoSettings
|
return nil, ErrNoSettings
|
||||||
@@ -80,7 +82,7 @@ func (s *SettingsService) Upsert(ctx context.Context, input UpsertSettingsInput)
|
|||||||
return nil, fmt.Errorf("invalid effective_from: %w", err)
|
return nil, fmt.Errorf("invalid effective_from: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := s.clock.Now().UnixMilli()
|
||||||
set := &domain.Settings{
|
set := &domain.Settings{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
EffectiveFrom: input.EffectiveFrom,
|
EffectiveFrom: input.EffectiveFrom,
|
||||||
@@ -135,7 +137,7 @@ func (s *SettingsService) UpdateSettings(ctx context.Context, id string, input U
|
|||||||
set.HoursPerWeek = input.HoursPerWeek
|
set.HoursPerWeek = input.HoursPerWeek
|
||||||
set.WorkdaysMask = input.WorkdaysMask
|
set.WorkdaysMask = input.WorkdaysMask
|
||||||
set.Timezone = input.Timezone
|
set.Timezone = input.Timezone
|
||||||
set.UpdatedAt = time.Now().UnixMilli()
|
set.UpdatedAt = s.clock.Now().UnixMilli()
|
||||||
|
|
||||||
if err := s.store.Update(ctx, set); err != nil {
|
if err := s.store.Update(ctx, set); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
@@ -30,6 +31,7 @@ type WeekService struct {
|
|||||||
}
|
}
|
||||||
rawDB *sql.DB
|
rawDB *sql.DB
|
||||||
tz *time.Location
|
tz *time.Location
|
||||||
|
clock clock.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWeekService(
|
func NewWeekService(
|
||||||
@@ -41,6 +43,7 @@ func NewWeekService(
|
|||||||
syncStore *store.SyncStore,
|
syncStore *store.SyncStore,
|
||||||
rawDB *sql.DB,
|
rawDB *sql.DB,
|
||||||
tz *time.Location,
|
tz *time.Location,
|
||||||
|
clk clock.Clock,
|
||||||
) *WeekService {
|
) *WeekService {
|
||||||
return &WeekService{
|
return &WeekService{
|
||||||
closedDays: closedDays,
|
closedDays: closedDays,
|
||||||
@@ -51,6 +54,7 @@ func NewWeekService(
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
rawDB: rawDB,
|
rawDB: rawDB,
|
||||||
tz: tz,
|
tz: tz,
|
||||||
|
clock: clk,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +189,7 @@ func (s *WeekService) CloseWeek(ctx context.Context, weekKey string) (*domain.Cl
|
|||||||
// Get settings effective at close time (today), not necessarily at the
|
// Get settings effective at close time (today), not necessarily at the
|
||||||
// start of the week. This ensures settings changes made mid-week are
|
// start of the week. This ensures settings changes made mid-week are
|
||||||
// reflected when the week is closed.
|
// reflected when the week is closed.
|
||||||
set, err := s.settings.Current(ctx, time.Now().In(s.tz).Format("2006-01-02"))
|
set, err := s.settings.Current(ctx, s.clock.Now().In(s.tz).Format("2006-01-02"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrNoSettings
|
return nil, ErrNoSettings
|
||||||
}
|
}
|
||||||
@@ -196,7 +200,7 @@ func (s *WeekService) CloseWeek(ctx context.Context, weekKey string) (*domain.Cl
|
|||||||
// Verify all past workdays that have entries are closed; collect worked ms.
|
// Verify all past workdays that have entries are closed; collect worked ms.
|
||||||
// Past workdays with no entries at all are skipped (they contribute 0h).
|
// Past workdays with no entries at all are skipped (they contribute 0h).
|
||||||
// Future workdays in the week are always skipped.
|
// Future workdays in the week are always skipped.
|
||||||
todayKey := time.Now().In(s.tz).Format("2006-01-02")
|
todayKey := s.clock.Now().In(s.tz).Format("2006-01-02")
|
||||||
var totalWorkedMs int64
|
var totalWorkedMs int64
|
||||||
for _, dk := range dayKeys {
|
for _, dk := range dayKeys {
|
||||||
t, _ := time.ParseInLocation("2006-01-02", dk, s.tz)
|
t, _ := time.ParseInLocation("2006-01-02", dk, s.tz)
|
||||||
@@ -226,7 +230,7 @@ func (s *WeekService) CloseWeek(ctx context.Context, weekKey string) (*domain.Cl
|
|||||||
// No entries, no closed record → untracked day, counts as 0h.
|
// No entries, no closed record → untracked day, counts as 0h.
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := s.clock.Now().UnixMilli()
|
||||||
cw := &domain.ClosedWeek{
|
cw := &domain.ClosedWeek{
|
||||||
WeekKey: weekKey,
|
WeekKey: weekKey,
|
||||||
ExpectedMs: expectedMs,
|
ExpectedMs: expectedMs,
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wotra/wotra/internal/clock"
|
||||||
"github.com/wotra/wotra/internal/domain"
|
"github.com/wotra/wotra/internal/domain"
|
||||||
"github.com/wotra/wotra/internal/service"
|
"github.com/wotra/wotra/internal/service"
|
||||||
"github.com/wotra/wotra/internal/store"
|
"github.com/wotra/wotra/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFullServices(t *testing.T) (*service.EntryService, *service.DayService, *service.WeekService, *service.SettingsService) {
|
// weekTestAnchor is a fixed Tuesday (2026-W20) used as "now" across week service tests.
|
||||||
|
var weekTestAnchor = time.Date(2026, 5, 13, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func newFullServices(t *testing.T) (*service.EntryService, *service.DayService, *service.WeekService, *service.SettingsService, *clock.FixedClock) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := store.Open(":memory:")
|
db, err := store.Open(":memory:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,12 +30,13 @@ func newFullServices(t *testing.T) (*service.EntryService, *service.DayService,
|
|||||||
syncStore := store.NewSyncStore(db)
|
syncStore := store.NewSyncStore(db)
|
||||||
settingsStore := store.NewSettingsStore(db)
|
settingsStore := store.NewSettingsStore(db)
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
|
clk := clock.Fixed(weekTestAnchor)
|
||||||
|
|
||||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz, clk)
|
||||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz, clk)
|
||||||
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz)
|
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz, clk)
|
||||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
settingsSvc := service.NewSettingsService(settingsStore, syncStore, clk)
|
||||||
return entrySvc, daySvc, weekSvc, settingsSvc
|
return entrySvc, daySvc, weekSvc, settingsSvc, clk
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWeekDayKeys(t *testing.T) {
|
func TestWeekDayKeys(t *testing.T) {
|
||||||
@@ -65,7 +70,7 @@ func TestWeekDayKeys(t *testing.T) {
|
|||||||
|
|
||||||
func TestCloseWeekBasic(t *testing.T) {
|
func TestCloseWeekBasic(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, weekSvc, _ := newFullServices(t)
|
entrySvc, daySvc, weekSvc, _, _ := newFullServices(t)
|
||||||
|
|
||||||
monday := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
monday := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
||||||
weekKey := "2024-W03"
|
weekKey := "2024-W03"
|
||||||
@@ -98,7 +103,7 @@ func TestCloseWeekBasic(t *testing.T) {
|
|||||||
|
|
||||||
func TestCloseWeekMissingDayFails(t *testing.T) {
|
func TestCloseWeekMissingDayFails(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, weekSvc, _ := newFullServices(t)
|
entrySvc, daySvc, weekSvc, _, _ := newFullServices(t)
|
||||||
|
|
||||||
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
@@ -125,7 +130,7 @@ func TestCloseWeekMissingDayFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestCloseWeekTwiceFails(t *testing.T) {
|
func TestCloseWeekTwiceFails(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, daySvc, weekSvc, _ := newFullServices(t)
|
_, daySvc, weekSvc, _, _ := newFullServices(t)
|
||||||
|
|
||||||
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
@@ -142,7 +147,7 @@ func TestCloseWeekTwiceFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestReopenWeek(t *testing.T) {
|
func TestReopenWeek(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, daySvc, weekSvc, _ := newFullServices(t)
|
_, daySvc, weekSvc, _, _ := newFullServices(t)
|
||||||
|
|
||||||
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
@@ -161,11 +166,13 @@ func TestReopenWeek(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloseWeekMidWeek(t *testing.T) {
|
func TestCloseWeekMidWeek(t *testing.T) {
|
||||||
|
// weekTestAnchor = 2026-05-13 (Tuesday, 2026-W20).
|
||||||
|
// Mon 2026-05-11 and Tue 2026-05-13 are past workdays; Wed-Fri are future.
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, daySvc, weekSvc, _ := newFullServices(t)
|
_, daySvc, weekSvc, _, clk := newFullServices(t)
|
||||||
|
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
now := time.Now().In(tz)
|
now := clk.Now().In(tz)
|
||||||
isoYear, isoWeek := now.ISOWeek()
|
isoYear, isoWeek := now.ISOWeek()
|
||||||
weekKey := fmt.Sprintf("%d-W%02d", isoYear, isoWeek)
|
weekKey := fmt.Sprintf("%d-W%02d", isoYear, isoWeek)
|
||||||
|
|
||||||
@@ -191,7 +198,7 @@ func TestWeekSnapshotUpdatesWhenDayReopened(t *testing.T) {
|
|||||||
// Regression: closing a day after the week is already closed must update
|
// Regression: closing a day after the week is already closed must update
|
||||||
// the frozen worked_ms/delta_ms on the closed week.
|
// the frozen worked_ms/delta_ms on the closed week.
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entrySvc, daySvc, weekSvc, _ := newFullServices(t)
|
entrySvc, daySvc, weekSvc, _, _ := newFullServices(t)
|
||||||
|
|
||||||
weekKey := "2024-W03"
|
weekKey := "2024-W03"
|
||||||
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
@@ -246,7 +253,7 @@ func TestWeekSnapshotUpdatesWhenDayReopened(t *testing.T) {
|
|||||||
|
|
||||||
func TestWeekServiceBalance(t *testing.T) {
|
func TestWeekServiceBalance(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, daySvc, weekSvc, _ := newFullServices(t)
|
_, daySvc, weekSvc, _, clk := newFullServices(t)
|
||||||
|
|
||||||
// Empty — no closed weeks, no adjustments.
|
// Empty — no closed weeks, no adjustments.
|
||||||
bal, err := weekSvc.Balance(ctx)
|
bal, err := weekSvc.Balance(ctx)
|
||||||
@@ -287,8 +294,8 @@ func TestWeekServiceBalance(t *testing.T) {
|
|||||||
t.Errorf("weeks-only total: want 0, got %+v", bal)
|
t.Errorf("weeks-only total: want 0, got %+v", bal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a +2h adjustment.
|
// Add a +2h adjustment; use the fixed clock's time for timestamps.
|
||||||
now := time.Now().UnixMilli()
|
now := clk.Now().UnixMilli()
|
||||||
adj, err := weekSvc.CreateAdjustment(ctx, &domain.BalanceAdjustment{
|
adj, err := weekSvc.CreateAdjustment(ctx, &domain.BalanceAdjustment{
|
||||||
ID: "adj-1", DeltaMs: 7_200_000, Note: "carry-over",
|
ID: "adj-1", DeltaMs: 7_200_000, Note: "carry-over",
|
||||||
EffectiveAt: now, CreatedAt: now, UpdatedAt: now,
|
EffectiveAt: now, CreatedAt: now, UpdatedAt: now,
|
||||||
|
|||||||
Reference in New Issue
Block a user