diff --git a/internal/infra/clock/clock.go b/internal/infra/clock/clock.go new file mode 100644 index 0000000..9ad7b64 --- /dev/null +++ b/internal/infra/clock/clock.go @@ -0,0 +1,18 @@ +// Package clock provides an injectable time source. Production code uses Real(); +// tests use NewFake(t) to make time-based logic deterministic. +package clock + +import "time" + +// Clock is the abstraction over the system clock. Implementations must be +// safe for concurrent use. +type Clock interface { + Now() time.Time +} + +type realClock struct{} + +// Real returns a Clock backed by time.Now. +func Real() Clock { return realClock{} } + +func (realClock) Now() time.Time { return time.Now() } diff --git a/internal/infra/clock/clock_test.go b/internal/infra/clock/clock_test.go new file mode 100644 index 0000000..6d26022 --- /dev/null +++ b/internal/infra/clock/clock_test.go @@ -0,0 +1,54 @@ +package clock + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRealClock_Now(t *testing.T) { + c := Real() + now := c.Now() + assert.WithinDuration(t, time.Now(), now, time.Second) +} + +func TestFakeClock_NowReturnsSetTime(t *testing.T) { + t0 := time.Date(2026, 5, 5, 10, 0, 0, 0, time.UTC) + c := NewFake(t0) + assert.Equal(t, t0, c.Now()) +} + +func TestFakeClock_Advance(t *testing.T) { + t0 := time.Date(2026, 5, 5, 10, 0, 0, 0, time.UTC) + c := NewFake(t0) + c.Advance(2 * time.Hour) + assert.Equal(t, t0.Add(2*time.Hour), c.Now()) +} + +func TestFakeClock_AdvanceConcurrent(t *testing.T) { + c := NewFake(time.Now()) + done := make(chan struct{}) + go func() { + for i := 0; i < 1000; i++ { + c.Advance(time.Microsecond) + } + close(done) + }() + for i := 0; i < 1000; i++ { + _ = c.Now() + } + <-done +} + +func TestFakeClock_Set(t *testing.T) { + c := NewFake(time.Date(2026, 5, 5, 10, 0, 0, 0, time.UTC)) + t1 := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC) + c.Set(t1) + assert.Equal(t, t1, c.Now()) +} + +var _ Clock = (*realClock)(nil) +var _ Clock = (*FakeClock)(nil) +var _ = require.New // keep import live; helper used by other packages diff --git a/internal/infra/clock/fake.go b/internal/infra/clock/fake.go new file mode 100644 index 0000000..b72f6d6 --- /dev/null +++ b/internal/infra/clock/fake.go @@ -0,0 +1,38 @@ +package clock + +import ( + "sync" + "time" +) + +// FakeClock is a controllable Clock for tests. Construct with NewFake; advance +// time with Advance; jump to a specific time with Set. Safe for concurrent use. +type FakeClock struct { + mu sync.Mutex + now time.Time +} + +// NewFake returns a FakeClock initialized to t. +func NewFake(t time.Time) *FakeClock { + return &FakeClock{now: t} +} + +func (c *FakeClock) Now() time.Time { + c.mu.Lock() + defer c.mu.Unlock() + return c.now +} + +// Advance moves the clock forward by d. +func (c *FakeClock) Advance(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.now = c.now.Add(d) +} + +// Set jumps the clock to t. +func (c *FakeClock) Set(t time.Time) { + c.mu.Lock() + defer c.mu.Unlock() + c.now = t +}