git.iko.soy/typeable/oauth2-proxy
on master help sign in
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

commitd3423408c7d5091f47e5b788dc39a3a9c0288cbe authorNick Meves <nick.meves@greenhouse.io> dateSun 18 Apr 2021, 10:25 (6 years ago) parent42475c2
6 files changed+565
CHANGELOG.md+1 go.mod+1 go.sum+2 pkg/clock/clock.go+164 pkg/clock/clock_suite_test.go+17 pkg/clock/clock_test.go+380
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v7.1.2
1010
11+- [#1136](https://github.com/oauth2-proxy/oauth2-proxy/pull/1136) Add clock package for better time mocking in tests (@NickMeves)
1112- [#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)
1213
1314# V7.1.2
go.mod+1
@@ -5,6 +5,7 @@ go 1.16
55require (
66 github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb
77 github.com/alicebob/miniredis/v2 v2.13.0
8+ github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0
89 github.com/bitly/go-simplejson v0.5.0
910 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
1011 github.com/coreos/go-oidc v2.2.1+incompatible
go.sum+2
@@ -38,6 +38,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
3838github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
3939github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
4040github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
41+github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0 h1:ROGOOFsMU1fh3kR94itIWlWiPLtgd4TA/qWi4+lL0GM=
42+github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
4143github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
4244github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
4345github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
pkg/clock/clock.gonew file+164
@@ -0,0 +1,164 @@
1+package clock
2+
3+import (
4+ "errors"
5+ "sync"
6+ "time"
7+
8+ clockapi "github.com/benbjohnson/clock"
9+)
10+
11+var (
12+ globalClock = clockapi.New()
13+ mu sync.Mutex
14+)
15+
16+// Set the global clock to a clockapi.Mock with the given time.Time
17+func Set(t time.Time) {
18+ mu.Lock()
19+ defer mu.Unlock()
20+ mock, ok := globalClock.(*clockapi.Mock)
21+ if !ok {
22+ mock = clockapi.NewMock()
23+ }
24+ mock.Set(t)
25+ globalClock = mock
26+}
27+
28+// Add moves the mocked global clock forward the given duration. It will error
29+// if the global clock is not mocked.
30+func Add(d time.Duration) error {
31+ mu.Lock()
32+ defer mu.Unlock()
33+ mock, ok := globalClock.(*clockapi.Mock)
34+ if !ok {
35+ return errors.New("time not mocked")
36+ }
37+ mock.Add(d)
38+ return nil
39+}
40+
41+// Reset sets the global clock to a pure time implementation. Returns any
42+// existing Mock if set in case lingering time operations are attached to it.
43+func Reset() *clockapi.Mock {
44+ mu.Lock()
45+ defer mu.Unlock()
46+ existing := globalClock
47+ globalClock = clockapi.New()
48+
49+ mock, ok := existing.(*clockapi.Mock)
50+ if !ok {
51+ return nil
52+ }
53+ return mock
54+}
55+
56+// Clock is a non-package level wrapper around time that supports stubbing.
57+// It will use its localized stubs (allowing for parallelized unit tests
58+// where package level stubbing would cause issues). It falls back to any
59+// package level time stubs for non-parallel, cross-package integration
60+// testing scenarios.
61+//
62+// If nothing is stubbed, it defaults to default time behavior in the time
63+// package.
64+type Clock struct {
65+ mock *clockapi.Mock
66+ sync.Mutex
67+}
68+
69+// Set sets the Clock to a clock.Mock at the given time.Time
70+func (c *Clock) Set(t time.Time) {
71+ c.Lock()
72+ defer c.Unlock()
73+ if c.mock == nil {
74+ c.mock = clockapi.NewMock()
75+ }
76+ c.mock.Set(t)
77+}
78+
79+// Add moves clock forward time.Duration if it is mocked. It will error
80+// if the clock is not mocked.
81+func (c *Clock) Add(d time.Duration) error {
82+ c.Lock()
83+ defer c.Unlock()
84+ if c.mock == nil {
85+ return errors.New("clock not mocked")
86+ }
87+ c.mock.Add(d)
88+ return nil
89+}
90+
91+// Reset removes local clock.Mock. Returns any existing Mock if set in case
92+// lingering time operations are attached to it.
93+func (c *Clock) Reset() *clockapi.Mock {
94+ c.Lock()
95+ defer c.Unlock()
96+ existing := c.mock
97+ c.mock = nil
98+ return existing
99+}
100+
101+func (c *Clock) After(d time.Duration) <-chan time.Time {
102+ m := c.mock
103+ if m == nil {
104+ return globalClock.After(d)
105+ }
106+ return m.After(d)
107+}
108+
109+func (c *Clock) AfterFunc(d time.Duration, f func()) *clockapi.Timer {
110+ m := c.mock
111+ if m == nil {
112+ return globalClock.AfterFunc(d, f)
113+ }
114+ return m.AfterFunc(d, f)
115+}
116+
117+func (c *Clock) Now() time.Time {
118+ m := c.mock
119+ if m == nil {
120+ return globalClock.Now()
121+ }
122+ return m.Now()
123+}
124+
125+func (c *Clock) Since(t time.Time) time.Duration {
126+ m := c.mock
127+ if m == nil {
128+ return globalClock.Since(t)
129+ }
130+ return m.Since(t)
131+}
132+
133+func (c *Clock) Sleep(d time.Duration) {
134+ m := c.mock
135+ if m == nil {
136+ globalClock.Sleep(d)
137+ return
138+ }
139+ m.Sleep(d)
140+}
141+
142+func (c *Clock) Tick(d time.Duration) <-chan time.Time {
143+ m := c.mock
144+ if m == nil {
145+ return globalClock.Tick(d)
146+ }
147+ return m.Tick(d)
148+}
149+
150+func (c *Clock) Ticker(d time.Duration) *clockapi.Ticker {
151+ m := c.mock
152+ if m == nil {
153+ return globalClock.Ticker(d)
154+ }
155+ return m.Ticker(d)
156+}
157+
158+func (c *Clock) Timer(d time.Duration) *clockapi.Timer {
159+ m := c.mock
160+ if m == nil {
161+ return globalClock.Timer(d)
162+ }
163+ return m.Timer(d)
164+}
pkg/clock/clock_suite_test.gonew file+17
@@ -0,0 +1,17 @@
1+package clock_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestClockSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+ logger.SetErrOutput(GinkgoWriter)
14+
15+ RegisterFailHandler(Fail)
16+ RunSpecs(t, "Clock")
17+}
pkg/clock/clock_test.gonew file+380
@@ -0,0 +1,380 @@
1+package clock_test
2+
3+import (
4+ "sync"
5+ "sync/atomic"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/clock"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/gomega"
11+)
12+
13+const (
14+ testGlobalEpoch = 1000000000
15+ testLocalEpoch = 1234567890
16+)
17+
18+var _ = Describe("Clock suite", func() {
19+ var testClock = clock.Clock{}
20+
21+ AfterEach(func() {
22+ clock.Reset()
23+ testClock.Reset()
24+ })
25+
26+ Context("Global time not overridden", func() {
27+ It("errors when trying to Add", func() {
28+ err := clock.Add(123 * time.Hour)
29+ Expect(err).To(HaveOccurred())
30+ })
31+
32+ Context("Clock not mocked via Set", func() {
33+ const (
34+ outsideTolerance = int32(0)
35+ withinTolerance = int32(1)
36+ )
37+
38+ It("uses time.After for After", func() {
39+ var tolerance int32
40+ go func() {
41+ time.Sleep(10 * time.Millisecond)
42+ atomic.StoreInt32(&tolerance, withinTolerance)
43+ }()
44+ go func() {
45+ time.Sleep(30 * time.Millisecond)
46+ atomic.StoreInt32(&tolerance, outsideTolerance)
47+ }()
48+
49+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
50+
51+ <-testClock.After(20 * time.Millisecond)
52+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
53+
54+ <-testClock.After(20 * time.Millisecond)
55+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
56+ })
57+
58+ It("uses time.AfterFunc for AfterFunc", func() {
59+ var tolerance int32
60+ go func() {
61+ time.Sleep(10 * time.Millisecond)
62+ atomic.StoreInt32(&tolerance, withinTolerance)
63+ }()
64+ go func() {
65+ time.Sleep(30 * time.Millisecond)
66+ atomic.StoreInt32(&tolerance, outsideTolerance)
67+ }()
68+
69+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
70+
71+ var wg sync.WaitGroup
72+ wg.Add(1)
73+ testClock.AfterFunc(20*time.Millisecond, func() {
74+ wg.Done()
75+ })
76+ wg.Wait()
77+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
78+
79+ wg.Add(1)
80+ testClock.AfterFunc(20*time.Millisecond, func() {
81+ wg.Done()
82+ })
83+ wg.Wait()
84+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
85+ })
86+
87+ It("uses time.Now for Now", func() {
88+ a := time.Now()
89+ b := testClock.Now()
90+ Expect(b.Sub(a).Round(10 * time.Millisecond)).To(Equal(0 * time.Millisecond))
91+ })
92+
93+ It("uses time.Since for Since", func() {
94+ past := time.Now().Add(-60 * time.Second)
95+ Expect(time.Since(past).Round(10 * time.Millisecond)).
96+ To(Equal(60 * time.Second))
97+ })
98+
99+ It("uses time.Sleep for Sleep", func() {
100+ var tolerance int32
101+ go func() {
102+ time.Sleep(10 * time.Millisecond)
103+ atomic.StoreInt32(&tolerance, withinTolerance)
104+ }()
105+ go func() {
106+ time.Sleep(30 * time.Millisecond)
107+ atomic.StoreInt32(&tolerance, outsideTolerance)
108+ }()
109+
110+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
111+
112+ testClock.Sleep(20 * time.Millisecond)
113+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
114+
115+ testClock.Sleep(20 * time.Millisecond)
116+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
117+ })
118+
119+ It("uses time.Tick for Tick", func() {
120+ var tolerance int32
121+ go func() {
122+ time.Sleep(10 * time.Millisecond)
123+ atomic.StoreInt32(&tolerance, withinTolerance)
124+ }()
125+ go func() {
126+ time.Sleep(50 * time.Millisecond)
127+ atomic.StoreInt32(&tolerance, outsideTolerance)
128+ }()
129+
130+ ch := testClock.Tick(20 * time.Millisecond)
131+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
132+ <-ch
133+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
134+ <-ch
135+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
136+ <-ch
137+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
138+ })
139+
140+ It("uses time.Ticker for Ticker", func() {
141+ var tolerance int32
142+ go func() {
143+ time.Sleep(10 * time.Millisecond)
144+ atomic.StoreInt32(&tolerance, withinTolerance)
145+ }()
146+ go func() {
147+ time.Sleep(50 * time.Millisecond)
148+ atomic.StoreInt32(&tolerance, outsideTolerance)
149+ }()
150+
151+ ticker := testClock.Ticker(20 * time.Millisecond)
152+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
153+ <-ticker.C
154+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
155+ <-ticker.C
156+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(withinTolerance))
157+ <-ticker.C
158+ Expect(atomic.LoadInt32(&tolerance)).To(Equal(outsideTolerance))
159+ })
160+
161+ It("errors if Add is used", func() {
162+ err := testClock.Add(100 * time.Second)
163+ Expect(err).To(HaveOccurred())
164+ })
165+ })
166+
167+ Context("Clock mocked via Set", func() {
168+ var now = time.Unix(testLocalEpoch, 0)
169+
170+ BeforeEach(func() {
171+ testClock.Set(now)
172+ })
173+
174+ It("mocks After", func() {
175+ var after int32
176+ ready := make(chan struct{})
177+ ch := testClock.After(10 * time.Second)
178+ go func(ch <-chan time.Time) {
179+ close(ready)
180+ <-ch
181+ atomic.StoreInt32(&after, 1)
182+ }(ch)
183+ <-ready
184+
185+ err := testClock.Add(9 * time.Second)
186+ Expect(err).ToNot(HaveOccurred())
187+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
188+
189+ err = testClock.Add(1 * time.Second)
190+ Expect(err).ToNot(HaveOccurred())
191+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
192+ })
193+
194+ It("mocks AfterFunc", func() {
195+ var after int32
196+ testClock.AfterFunc(10*time.Second, func() {
197+ atomic.StoreInt32(&after, 1)
198+ })
199+
200+ err := testClock.Add(9 * time.Second)
201+ Expect(err).ToNot(HaveOccurred())
202+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
203+
204+ err = testClock.Add(1 * time.Second)
205+ Expect(err).ToNot(HaveOccurred())
206+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
207+ })
208+
209+ It("mocks AfterFunc with a stopped timer", func() {
210+ var after int32
211+ timer := testClock.AfterFunc(10*time.Second, func() {
212+ atomic.StoreInt32(&after, 1)
213+ })
214+ timer.Stop()
215+
216+ err := testClock.Add(11 * time.Second)
217+ Expect(err).ToNot(HaveOccurred())
218+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
219+ })
220+
221+ It("mocks Now", func() {
222+ Expect(testClock.Now()).To(Equal(now))
223+ err := testClock.Add(123 * time.Hour)
224+ Expect(err).ToNot(HaveOccurred())
225+ Expect(testClock.Now()).To(Equal(now.Add(123 * time.Hour)))
226+ })
227+
228+ It("mocks Since", func() {
229+ Expect(testClock.Since(time.Unix(testLocalEpoch-100, 0))).
230+ To(Equal(100 * time.Second))
231+ })
232+
233+ It("mocks Sleep", func() {
234+ var after int32
235+ ready := make(chan struct{})
236+ go func() {
237+ close(ready)
238+ testClock.Sleep(10 * time.Second)
239+ atomic.StoreInt32(&after, 1)
240+ }()
241+ <-ready
242+
243+ err := testClock.Add(9 * time.Second)
244+ Expect(err).ToNot(HaveOccurred())
245+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
246+
247+ err = testClock.Add(1 * time.Second)
248+ Expect(err).ToNot(HaveOccurred())
249+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
250+ })
251+
252+ It("mocks Tick", func() {
253+ var ticks int32
254+ ready := make(chan struct{})
255+ go func() {
256+ close(ready)
257+ tick := testClock.Tick(10 * time.Second)
258+ for ticks < 5 {
259+ <-tick
260+ atomic.AddInt32(&ticks, 1)
261+ }
262+ }()
263+ <-ready
264+
265+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
266+
267+ err := testClock.Add(9 * time.Second)
268+ Expect(err).ToNot(HaveOccurred())
269+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
270+
271+ err = testClock.Add(1 * time.Second)
272+ Expect(err).ToNot(HaveOccurred())
273+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(1)))
274+
275+ err = testClock.Add(30 * time.Second)
276+ Expect(err).ToNot(HaveOccurred())
277+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(4)))
278+
279+ err = testClock.Add(10 * time.Second)
280+ Expect(err).ToNot(HaveOccurred())
281+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(5)))
282+ })
283+
284+ It("mocks Ticker", func() {
285+ var ticks int32
286+ ready := make(chan struct{})
287+ go func() {
288+ ticker := testClock.Ticker(10 * time.Second)
289+ close(ready)
290+ for ticks < 5 {
291+ <-ticker.C
292+ atomic.AddInt32(&ticks, 1)
293+ }
294+ }()
295+ <-ready
296+
297+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
298+
299+ err := testClock.Add(9 * time.Second)
300+ Expect(err).ToNot(HaveOccurred())
301+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(0)))
302+
303+ err = testClock.Add(1 * time.Second)
304+ Expect(err).ToNot(HaveOccurred())
305+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(1)))
306+
307+ err = testClock.Add(30 * time.Second)
308+ Expect(err).ToNot(HaveOccurred())
309+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(4)))
310+
311+ err = testClock.Add(10 * time.Second)
312+ Expect(err).ToNot(HaveOccurred())
313+ Expect(atomic.LoadInt32(&ticks)).To(Equal(int32(5)))
314+ })
315+
316+ It("mocks Timer", func() {
317+ var after int32
318+ ready := make(chan struct{})
319+ go func() {
320+ timer := testClock.Timer(10 * time.Second)
321+ close(ready)
322+ <-timer.C
323+ atomic.AddInt32(&after, 1)
324+ }()
325+ <-ready
326+
327+ err := testClock.Add(9 * time.Second)
328+ Expect(err).ToNot(HaveOccurred())
329+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(0)))
330+
331+ err = testClock.Add(1 * time.Second)
332+ Expect(err).ToNot(HaveOccurred())
333+ Expect(atomic.LoadInt32(&after)).To(Equal(int32(1)))
334+ })
335+ })
336+ })
337+
338+ Context("Global time overridden", func() {
339+ var (
340+ globalNow = time.Unix(testGlobalEpoch, 0)
341+ localNow = time.Unix(testLocalEpoch, 0)
342+ )
343+
344+ BeforeEach(func() {
345+ clock.Set(globalNow)
346+ })
347+
348+ Context("Clock not mocked via Set", func() {
349+ It("uses globally mocked Now", func() {
350+ Expect(testClock.Now()).To(Equal(globalNow))
351+ err := clock.Add(123 * time.Hour)
352+ Expect(err).ToNot(HaveOccurred())
353+ Expect(testClock.Now()).To(Equal(globalNow.Add(123 * time.Hour)))
354+ })
355+
356+ It("errors when Add is called on the local Clock", func() {
357+ err := testClock.Add(100 * time.Hour)
358+ Expect(err).To(HaveOccurred())
359+ })
360+ })
361+
362+ Context("Clock is mocked via Set", func() {
363+ BeforeEach(func() {
364+ testClock.Set(localNow)
365+ })
366+
367+ It("uses the local mock and ignores the global", func() {
368+ Expect(testClock.Now()).To(Equal(localNow))
369+
370+ err := clock.Add(456 * time.Hour)
371+ Expect(err).ToNot(HaveOccurred())
372+
373+ err = testClock.Add(123 * time.Hour)
374+ Expect(err).ToNot(HaveOccurred())
375+
376+ Expect(testClock.Now()).To(Equal(localNow.Add(123 * time.Hour)))
377+ })
378+ })
379+ })
380+})