Add a clock package for better time mocking (#1136)

* Add a clock package for better time mocking

* Make Clock a struct so it doesn't need initialization

* Test clock package

* Use atomic for live time tests

* Refer to same clock.Mock throughout methods
This commit is contained in:
Nick Meves 2021-04-18 10:25:57 -07:00 committed by GitHub
parent 42475c28f7
commit d3423408c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 565 additions and 0 deletions

View File

@ -8,6 +8,7 @@
## Changes since v7.1.2
- [#1136](https://github.com/oauth2-proxy/oauth2-proxy/pull/1136) Add clock package for better time mocking in tests (@NickMeves)
- [#947](https://github.com/oauth2-proxy/oauth2-proxy/pull/947) Multiple provider ingestion and validation in alpha options (first stage: [#926](https://github.com/oauth2-proxy/oauth2-proxy/issues/926)) (@yanasega)
# V7.1.2

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.16
require (
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
github.com/alicebob/miniredis/v2 v2.13.0
github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/coreos/go-oidc v2.2.1+incompatible

2
go.sum
View File

@ -38,6 +38,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0 h1:ROGOOFsMU1fh3kR94itIWlWiPLtgd4TA/qWi4+lL0GM=
github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

164
pkg/clock/clock.go Normal file
View File

@ -0,0 +1,164 @@
package clock
import (
"errors"
"sync"
"time"
clockapi "github.com/benbjohnson/clock"
)
var (
globalClock = clockapi.New()
mu sync.Mutex
)
// Set the global clock to a clockapi.Mock with the given time.Time
func Set(t time.Time) {
mu.Lock()
defer mu.Unlock()
mock, ok := globalClock.(*clockapi.Mock)
if !ok {
mock = clockapi.NewMock()
}
mock.Set(t)
globalClock = mock
}
// Add moves the mocked global clock forward the given duration. It will error
// if the global clock is not mocked.
func Add(d time.Duration) error {
mu.Lock()
defer mu.Unlock()
mock, ok := globalClock.(*clockapi.Mock)
if !ok {
return errors.New("time not mocked")
}
mock.Add(d)
return nil
}
// Reset sets the global clock to a pure time implementation. Returns any
// existing Mock if set in case lingering time operations are attached to it.
func Reset() *clockapi.Mock {
mu.Lock()
defer mu.Unlock()
existing := globalClock
globalClock = clockapi.New()
mock, ok := existing.(*clockapi.Mock)
if !ok {
return nil
}
return mock
}
// Clock is a non-package level wrapper around time that supports stubbing.
// It will use its localized stubs (allowing for parallelized unit tests
// where package level stubbing would cause issues). It falls back to any
// package level time stubs for non-parallel, cross-package integration
// testing scenarios.
//
// If nothing is stubbed, it defaults to default time behavior in the time
// package.
type Clock struct {
mock *clockapi.Mock
sync.Mutex
}
// Set sets the Clock to a clock.Mock at the given time.Time
func (c *Clock) Set(t time.Time) {
c.Lock()
defer c.Unlock()
if c.mock == nil {
c.mock = clockapi.NewMock()
}
c.mock.Set(t)
}
// Add moves clock forward time.Duration if it is mocked. It will error
// if the clock is not mocked.
func (c *Clock) Add(d time.Duration) error {
c.Lock()
defer c.Unlock()
if c.mock == nil {
return errors.New("clock not mocked")
}
c.mock.Add(d)
return nil
}
// Reset removes local clock.Mock. Returns any existing Mock if set in case
// lingering time operations are attached to it.
func (c *Clock) Reset() *clockapi.Mock {
c.Lock()
defer c.Unlock()
existing := c.mock
c.mock = nil
return existing
}
func (c *Clock) After(d time.Duration) <-chan time.Time {
m := c.mock
if m == nil {
return globalClock.After(d)
}
return m.After(d)
}
func (c *Clock) AfterFunc(d time.Duration, f func()) *clockapi.Timer {
m := c.mock
if m == nil {
return globalClock.AfterFunc(d, f)
}
return m.AfterFunc(d, f)
}
func (c *Clock) Now() time.Time {
m := c.mock
if m == nil {
return globalClock.Now()
}
return m.Now()
}
func (c *Clock) Since(t time.Time) time.Duration {
m := c.mock
if m == nil {
return globalClock.Since(t)
}
return m.Since(t)
}
func (c *Clock) Sleep(d time.Duration) {
m := c.mock
if m == nil {
globalClock.Sleep(d)
return
}
m.Sleep(d)
}
func (c *Clock) Tick(d time.Duration) <-chan time.Time {
m := c.mock
if m == nil {
return globalClock.Tick(d)
}
return m.Tick(d)
}
func (c *Clock) Ticker(d time.Duration) *clockapi.Ticker {
m := c.mock
if m == nil {
return globalClock.Ticker(d)
}
return m.Ticker(d)
}
func (c *Clock) Timer(d time.Duration) *clockapi.Timer {
m := c.mock
if m == nil {
return globalClock.Timer(d)
}
return m.Timer(d)
}

View File

@ -0,0 +1,17 @@
package clock_test
import (
"testing"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestClockSuite(t *testing.T) {
logger.SetOutput(GinkgoWriter)
logger.SetErrOutput(GinkgoWriter)
RegisterFailHandler(Fail)
RunSpecs(t, "Clock")
}

380
pkg/clock/clock_test.go Normal file
View File

@ -0,0 +1,380 @@
package clock_test
import (
"sync"
"sync/atomic"
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/clock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const (
testGlobalEpoch = 1000000000
testLocalEpoch = 1234567890
)
var _ = Describe("Clock suite", func() {
var testClock = clock.Clock{}
AfterEach(func() {
clock.Reset()
testClock.Reset()
})
Context("Global time not overridden", func() {
It("errors when trying to Add", func() {
err := clock.Add(123 * time.Hour)
Expect(err).To(HaveOccurred())
})
Context("Clock not mocked via Set", func() {
const (
outsideTolerance = int32(0)
withinTolerance = int32(1)
)
It("uses time.After for After", func() {
var tolerance int32
go func() {
time.Sleep(10 * time.Millisecond)
atomic.StoreInt32(&tolerance, withinTolerance)
}()
go func() {
time.Sleep(30 * time.Millisecond)
atomic.StoreInt32(&tolerance, outsideTolerance)
}()
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
<-testClock.After(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
<-testClock.After(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
})
It("uses time.AfterFunc for AfterFunc", func() {
var tolerance int32
go func() {
time.Sleep(10 * time.Millisecond)
atomic.StoreInt32(&tolerance, withinTolerance)
}()
go func() {
time.Sleep(30 * time.Millisecond)
atomic.StoreInt32(&tolerance, outsideTolerance)
}()
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
var wg sync.WaitGroup
wg.Add(1)
testClock.AfterFunc(20*time.Millisecond, func() {
wg.Done()
})
wg.Wait()
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
wg.Add(1)
testClock.AfterFunc(20*time.Millisecond, func() {
wg.Done()
})
wg.Wait()
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
})
It("uses time.Now for Now", func() {
a := time.Now()
b := testClock.Now()
Expect(b.Sub(a).Round(10 * time.Millisecond)).To(Equal(0 * time.Millisecond))
})
It("uses time.Since for Since", func() {
past := time.Now().Add(-60 * time.Second)
Expect(time.Since(past).Round(10 * time.Millisecond)).
To(Equal(60 * time.Second))
})
It("uses time.Sleep for Sleep", func() {
var tolerance int32
go func() {
time.Sleep(10 * time.Millisecond)
atomic.StoreInt32(&tolerance, withinTolerance)
}()
go func() {
time.Sleep(30 * time.Millisecond)
atomic.StoreInt32(&tolerance, outsideTolerance)
}()
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
testClock.Sleep(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
testClock.Sleep(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
})
It("uses time.Tick for Tick", func() {
var tolerance int32
go func() {
time.Sleep(10 * time.Millisecond)
atomic.StoreInt32(&tolerance, withinTolerance)
}()
go func() {
time.Sleep(50 * time.Millisecond)
atomic.StoreInt32(&tolerance, outsideTolerance)
}()
ch := testClock.Tick(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
<-ch
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
<-ch
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
<-ch
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
})
It("uses time.Ticker for Ticker", func() {
var tolerance int32
go func() {
time.Sleep(10 * time.Millisecond)
atomic.StoreInt32(&tolerance, withinTolerance)
}()
go func() {
time.Sleep(50 * time.Millisecond)
atomic.StoreInt32(&tolerance, outsideTolerance)
}()
ticker := testClock.Ticker(20 * time.Millisecond)
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
<-ticker.C
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
<-ticker.C
Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
<-ticker.C
Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
})
It("errors if Add is used", func() {
err := testClock.Add(100 * time.Second)
Expect(err).To(HaveOccurred())
})
})
Context("Clock mocked via Set", func() {
var now = time.Unix(testLocalEpoch, 0)
BeforeEach(func() {
testClock.Set(now)
})
It("mocks After", func() {
var after int32
ready := make(chan struct{})
ch := testClock.After(10 * time.Second)
go func(ch <-chan time.Time) {
close(ready)
<-ch
atomic.StoreInt32(&after, 1)
}(ch)
<-ready
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
})
It("mocks AfterFunc", func() {
var after int32
testClock.AfterFunc(10*time.Second, func() {
atomic.StoreInt32(&after, 1)
})
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
})
It("mocks AfterFunc with a stopped timer", func() {
var after int32
timer := testClock.AfterFunc(10*time.Second, func() {
atomic.StoreInt32(&after, 1)
})
timer.Stop()
err := testClock.Add(11 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
})
It("mocks Now", func() {
Expect(testClock.Now()).To(Equal(now))
err := testClock.Add(123 * time.Hour)
Expect(err).ToNot(HaveOccurred())
Expect(testClock.Now()).To(Equal(now.Add(123 * time.Hour)))
})
It("mocks Since", func() {
Expect(testClock.Since(time.Unix(testLocalEpoch-100, 0))).
To(Equal(100 * time.Second))
})
It("mocks Sleep", func() {
var after int32
ready := make(chan struct{})
go func() {
close(ready)
testClock.Sleep(10 * time.Second)
atomic.StoreInt32(&after, 1)
}()
<-ready
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
})
It("mocks Tick", func() {
var ticks int32
ready := make(chan struct{})
go func() {
close(ready)
tick := testClock.Tick(10 * time.Second)
for ticks < 5 {
<-tick
atomic.AddInt32(&ticks, 1)
}
}()
<-ready
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(1)))
err = testClock.Add(30 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(4)))
err = testClock.Add(10 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(5)))
})
It("mocks Ticker", func() {
var ticks int32
ready := make(chan struct{})
go func() {
ticker := testClock.Ticker(10 * time.Second)
close(ready)
for ticks < 5 {
<-ticker.C
atomic.AddInt32(&ticks, 1)
}
}()
<-ready
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(1)))
err = testClock.Add(30 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(4)))
err = testClock.Add(10 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(5)))
})
It("mocks Timer", func() {
var after int32
ready := make(chan struct{})
go func() {
timer := testClock.Timer(10 * time.Second)
close(ready)
<-timer.C
atomic.AddInt32(&after, 1)
}()
<-ready
err := testClock.Add(9 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
err = testClock.Add(1 * time.Second)
Expect(err).ToNot(HaveOccurred())
Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
})
})
})
Context("Global time overridden", func() {
var (
globalNow = time.Unix(testGlobalEpoch, 0)
localNow = time.Unix(testLocalEpoch, 0)
)
BeforeEach(func() {
clock.Set(globalNow)
})
Context("Clock not mocked via Set", func() {
It("uses globally mocked Now", func() {
Expect(testClock.Now()).To(Equal(globalNow))
err := clock.Add(123 * time.Hour)
Expect(err).ToNot(HaveOccurred())
Expect(testClock.Now()).To(Equal(globalNow.Add(123 * time.Hour)))
})
It("errors when Add is called on the local Clock", func() {
err := testClock.Add(100 * time.Hour)
Expect(err).To(HaveOccurred())
})
})
Context("Clock is mocked via Set", func() {
BeforeEach(func() {
testClock.Set(localNow)
})
It("uses the local mock and ignores the global", func() {
Expect(testClock.Now()).To(Equal(localNow))
err := clock.Add(456 * time.Hour)
Expect(err).ToNot(HaveOccurred())
err = testClock.Add(123 * time.Hour)
Expect(err).ToNot(HaveOccurred())
Expect(testClock.Now()).To(Equal(localNow.Add(123 * time.Hour)))
})
})
})
})