From 2a0f734c7be991c9f6083a38f2fddd45e8389e88 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 22 Jan 2026 14:01:26 +0000 Subject: [PATCH 1/6] fix(poller): introduce wait interval multiplier Signed-off-by: Babak K. Shandiz --- device/device_flow.go | 74 ++++++++- device/device_flow_test.go | 329 ++++++++++++++++++++++++++++++++++++- device/poller.go | 8 +- 3 files changed, 404 insertions(+), 7 deletions(-) diff --git a/device/device_flow.go b/device/device_flow.go index 5fe1767..e3619e2 100644 --- a/device/device_flow.go +++ b/device/device_flow.go @@ -143,11 +143,44 @@ type WaitOptions struct { // GrantType overrides the default value specified by OAuth 2.0 Device Code. Optional. GrantType string - newPoller pollerFactory + newPoller pollerFactory + calculateTimeDriftRatioF func(tstart, tstop time.Time) float64 } +const ( + primaryIntervalMultiplier = 1.2 + secondaryIntervalMultiplier = 1.4 +) + // Wait polls the server at uri until authorization completes. func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api.AccessToken, error) { + // We know that in virtualised environments (e.g. WSL or VMs), the monotonic + // clock, which is the source of time measurements in Go, can run faster than + // real time. So, polling intervals should be adjusted to avoid falling into + // an endless loop of "slow_down" errors. See the following issue in cli/cli + // for more context (especially what's after this particular comment): + // - https://github.com/cli/cli/issues/9370#issuecomment-3759706125 + // + // We've observed ~10% faster ticking, thanks to community, but a chat with + // AI suggests it's typically between 5-15% on WSL, and can get up to 30% in + // worst cases. There are issues reported on the WSL repo, but I couldn't + // find any documented/conclusive data about this. + // + // See more: + // - https://github.com/microsoft/WSL/issues/12583 + // + // What we're doing here is to play on the safe side by applying a default + // 20% increase to the polling interval from the start. That is, instead of + // 5s, we begin with 6s waits. This should resolve most cases without any + // "slow_down" errors. However, upon receiving a "slow_down" from the OAuth + // server, we will bump the safety margin to 40%. This will eliminate further + // "slow_down"s in most cases. + // + // We also bail out if we receive more than two "slow_down" errors, as that's + // probably an indication of severe clock drift. In such cases, we'll check + // the time drift between the monotonic and the wall clocks and report it in + // the error message to hint the user at the root cause. + baseCheckInterval := time.Duration(opts.DeviceCode.Interval) * time.Second expiresIn := time.Duration(opts.DeviceCode.ExpiresIn) * time.Second grantType := opts.GrantType @@ -161,11 +194,23 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api } _, poll := makePoller(ctx, baseCheckInterval, expiresIn) + calculateTimeDriftRatioF := opts.calculateTimeDriftRatioF + if calculateTimeDriftRatioF == nil { + calculateTimeDriftRatioF = calculateTimeDriftRatio + } + + multiplier := primaryIntervalMultiplier + + var slowDowns int for { - if err := poll.Wait(); err != nil { + tstart := time.Now() + + if err := poll.Wait(multiplier); err != nil { return nil, err } + tstop := time.Now() + values := url.Values{ "client_id": {opts.ClientID}, "device_code": {opts.DeviceCode.DeviceCode}, @@ -199,6 +244,23 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api } if apiError.Code == "slow_down" { + slowDowns++ + + // See if we can detect a drift between the monotonic and wall clocks. + driftRatio := calculateTimeDriftRatioF(tstart, tstop) + + // A positive drift ratio means the monotonic clock is faster than + // the wall clock. We should avoid hanging here in an endless loop of + // slow-downs. + // + // A negative drift (monotonic clock is slower than the wall clock), + // should not cause slow_down errors, unless the slow monotonic clock + // is still ticking faster than the OAuth server's clock. For such + // cases we tolerate a few more slow-downs. + if slowDowns > 2 && driftRatio > 0.05 || slowDowns > 4 && driftRatio < 0 { + return nil, fmt.Errorf("received too many slow_down responses; detected clock drift of roughly %.0f%% between monotonic and wall clocks; please ensure your system clock is accurate", driftRatio*100) + } + // Based on the RFC spec, we must add 5 seconds to our current polling interval. // (See https://www.rfc-editor.org/rfc/rfc8628#section-3.5) newInterval := poll.GetInterval() + 5*time.Second @@ -213,9 +275,17 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api } poll.SetInterval(newInterval) + multiplier = secondaryIntervalMultiplier continue } return nil, err } } + +func calculateTimeDriftRatio(tstart, tstop time.Time) float64 { + elapsedWall := tstop.UnixNano() - tstart.UnixNano() + elapsedMono := tstop.Sub(tstart).Nanoseconds() + drift := elapsedMono - elapsedWall + return float64(drift) / float64(elapsedWall) +} diff --git a/device/device_flow_test.go b/device/device_flow_test.go index a24de11..032e314 100644 --- a/device/device_flow_test.go +++ b/device/device_flow_test.go @@ -297,6 +297,12 @@ func TestRequestCode(t *testing.T) { } func TestPollToken(t *testing.T) { + assertWaitMultipliers := func(t *testing.T, want, got []float64) { + if !reflect.DeepEqual(got, want) { + t.Errorf("unexpected wait multipliers; got %#v, want %#v", got, want) + } + } + singletonFakePoller := func(maxWaits int) pollerFactory { var instance *fakePoller return func(ctx context.Context, interval, expiresIn time.Duration) (context.Context, poller) { @@ -312,6 +318,17 @@ func TestPollToken(t *testing.T) { } } + newCalculateTimeDriftRatioStub := func(driftRatios ...float64) func(tstart, tstop time.Time) float64 { + var count int + return func(_, _ time.Time) float64 { + count++ + if count > len(driftRatios) { + return 0 + } + return driftRatios[count-1] + } + } + type args struct { http apiClient url string @@ -324,7 +341,6 @@ func TestPollToken(t *testing.T) { wantErr string assertFunc func(*testing.T, args) posts []postArgs - slept time.Duration }{ { name: "success", @@ -383,6 +399,7 @@ func TestPollToken(t *testing.T) { if poller.(*fakePoller).updatedIntervals != nil { t.Errorf("no interval change expected = %v", poller.(*fakePoller).updatedIntervals) } + assertWaitMultipliers(t, []float64{1.2, 1.2}, poller.(*fakePoller).waitMultipliers) }, }, { @@ -457,6 +474,7 @@ func TestPollToken(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Errorf("unexpected updated intervals = %v, want %v", got, want) } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, { @@ -544,6 +562,7 @@ func TestPollToken(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Errorf("unexpected updated intervals = %v, want %v", got, want) } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, { @@ -618,6 +637,7 @@ func TestPollToken(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Errorf("unexpected updated intervals = %v, want %v", got, want) } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, { @@ -705,6 +725,309 @@ func TestPollToken(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Errorf("unexpected updated intervals = %v, want %v", got, want) } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) + }, + }, + { + name: "success with multiple slow downs, and acceptable monotonic clock drift", + args: args{ + http: apiClient{ + stubs: []apiStub{ + { + body: "error=authorization_pending", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "access_token=123abc", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + }, + }, + url: "https://github.com/oauth", + opts: WaitOptions{ + ClientID: "CLIENT-ID", + DeviceCode: &CodeResponse{ + DeviceCode: "DEVIC", + UserCode: "123-abc", + VerificationURI: "http://verify.me", + ExpiresIn: 99, + Interval: 5, + }, + newPoller: singletonFakePoller(5), + calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.0499, 0.0499, 0.0499), + }, + }, + want: &api.AccessToken{ + Token: "123abc", + }, + posts: []postArgs{ + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + }, + assertFunc: func(t *testing.T, a args) { + // Get the created poller + _, poller := a.opts.newPoller(context.Background(), 0, 0) + got := poller.(*fakePoller).updatedIntervals + want := []time.Duration{10 * time.Second, 15 * time.Second, 20 * time.Second} + if !reflect.DeepEqual(got, want) { + t.Errorf("unexpected updated intervals = %v, want %v", got, want) + } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) + }, + }, + { + name: "failure with multiple slow downs, due to high clock drift (mono slower)", + args: args{ + http: apiClient{ + stubs: []apiStub{ + { + body: "error=authorization_pending", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + }, + }, + url: "https://github.com/oauth", + opts: WaitOptions{ + ClientID: "CLIENT-ID", + DeviceCode: &CodeResponse{ + DeviceCode: "DEVIC", + UserCode: "123-abc", + VerificationURI: "http://verify.me", + ExpiresIn: 99, + Interval: 5, + }, + newPoller: singletonFakePoller(4), + calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.051, 0.051, 0.051), + }, + }, + wantErr: `received too many slow_down responses; detected clock drift of roughly 5% between monotonic and wall clocks; please ensure your system clock is accurate`, + posts: []postArgs{ + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + }, + assertFunc: func(t *testing.T, a args) { + // Get the created poller + _, poller := a.opts.newPoller(context.Background(), 0, 0) + got := poller.(*fakePoller).updatedIntervals + want := []time.Duration{10 * time.Second, 15 * time.Second} + if !reflect.DeepEqual(got, want) { + t.Errorf("unexpected updated intervals = %v, want %v", got, want) + } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) + }, + }, + { + name: "failure with multiple slow downs, due to high clock drift (mono slower)", + args: args{ + http: apiClient{ + stubs: []apiStub{ + { + body: "error=authorization_pending", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + { + body: "error=slow_down", + status: 200, + contentType: "application/x-www-form-urlencoded; charset=utf-8", + }, + }, + }, + url: "https://github.com/oauth", + opts: WaitOptions{ + ClientID: "CLIENT-ID", + DeviceCode: &CodeResponse{ + DeviceCode: "DEVIC", + UserCode: "123-abc", + VerificationURI: "http://verify.me", + ExpiresIn: 99, + Interval: 5, + }, + newPoller: singletonFakePoller(6), + calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(-0.01, -0.01, -0.01, -0.01, -0.01), + }, + }, + wantErr: `received too many slow_down responses; detected clock drift of roughly -1% between monotonic and wall clocks; please ensure your system clock is accurate`, + posts: []postArgs{ + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + { + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + }, + assertFunc: func(t *testing.T, a args) { + // Get the created poller + _, poller := a.opts.newPoller(context.Background(), 0, 0) + got := poller.(*fakePoller).updatedIntervals + want := []time.Duration{10 * time.Second, 15 * time.Second, 20 * time.Second, 25 * time.Second} + if !reflect.DeepEqual(got, want) { + t.Errorf("unexpected updated intervals = %v, want %v", got, want) + } + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, { @@ -866,6 +1189,7 @@ type fakePoller struct { maxWaits int count int updatedIntervals []time.Duration + waitMultipliers []float64 } func (p *fakePoller) GetInterval() time.Duration { @@ -877,10 +1201,11 @@ func (p *fakePoller) SetInterval(d time.Duration) { p.updatedIntervals = append(p.updatedIntervals, d) } -func (p *fakePoller) Wait() error { +func (p *fakePoller) Wait(multiplier float64) error { if p.count == p.maxWaits { return errors.New("context deadline exceeded") } + p.waitMultipliers = append(p.waitMultipliers, multiplier) p.count++ return nil } diff --git a/device/poller.go b/device/poller.go index f74399a..a8362be 100644 --- a/device/poller.go +++ b/device/poller.go @@ -2,13 +2,14 @@ package device import ( "context" + "math" "time" ) type poller interface { GetInterval() time.Duration SetInterval(time.Duration) - Wait() error + Wait(multiplier float64) error Cancel() } @@ -37,8 +38,9 @@ func (p *intervalPoller) SetInterval(d time.Duration) { p.interval = d } -func (p *intervalPoller) Wait() error { - t := time.NewTimer(p.interval) +func (p *intervalPoller) Wait(multiplier float64) error { + interval := time.Duration(math.Ceil(float64(p.interval) * multiplier)) + t := time.NewTimer(interval) select { case <-p.ctx.Done(): t.Stop() From cbeeb2f5972c810a087563d9f8124850e01e582a Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 28 Jan 2026 11:56:56 +0000 Subject: [PATCH 2/6] refactor: collapse repeated post args Signed-off-by: Babak K. Shandiz --- device/device_flow_test.go | 344 ++++++++----------------------------- 1 file changed, 71 insertions(+), 273 deletions(-) diff --git a/device/device_flow_test.go b/device/device_flow_test.go index 032e314..8be386f 100644 --- a/device/device_flow_test.go +++ b/device/device_flow_test.go @@ -297,6 +297,14 @@ func TestRequestCode(t *testing.T) { } func TestPollToken(t *testing.T) { + repeatPostArgs := func(count int, args postArgs) []postArgs { + posts := make([]postArgs, count) + for i := 0; i < count; i++ { + posts[i] = args + } + return posts + } + assertWaitMultipliers := func(t *testing.T, want, got []float64) { if !reflect.DeepEqual(got, want) { t.Errorf("unexpected wait multipliers; got %#v, want %#v", got, want) @@ -375,24 +383,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(2, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -440,32 +438,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(3, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -520,40 +500,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(4, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -603,32 +557,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(3, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -683,40 +619,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(4, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -777,48 +687,14 @@ func TestPollToken(t *testing.T) { want: &api.AccessToken{ Token: "123abc", }, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(5, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -872,40 +748,14 @@ func TestPollToken(t *testing.T) { }, }, wantErr: `received too many slow_down responses; detected clock drift of roughly 5% between monotonic and wall clocks; please ensure your system clock is accurate`, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(4, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -969,56 +819,14 @@ func TestPollToken(t *testing.T) { }, }, wantErr: `received too many slow_down responses; detected clock drift of roughly -1% between monotonic and wall clocks; please ensure your system clock is accurate`, - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(6, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), assertFunc: func(t *testing.T, a args) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) @@ -1103,24 +911,14 @@ func TestPollToken(t *testing.T) { }, }, wantErr: "context deadline exceeded", - posts: []postArgs{ - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }, - { - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, + posts: repeatPostArgs(2, postArgs{ + url: "https://github.com/oauth", + params: url.Values{ + "client_id": {"CLIENT-ID"}, + "device_code": {"DEVIC"}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, }, - }, + }), }, { name: "access denied", From 36cda407157275a8c78ff9591d1fe14b92cebaf5 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 28 Jan 2026 12:05:37 +0000 Subject: [PATCH 3/6] fix: tune clock drift for slow-mono case Signed-off-by: Babak K. Shandiz --- device/device_flow.go | 2 +- device/device_flow_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/device/device_flow.go b/device/device_flow.go index e3619e2..6bb84c3 100644 --- a/device/device_flow.go +++ b/device/device_flow.go @@ -257,7 +257,7 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api // should not cause slow_down errors, unless the slow monotonic clock // is still ticking faster than the OAuth server's clock. For such // cases we tolerate a few more slow-downs. - if slowDowns > 2 && driftRatio > 0.05 || slowDowns > 4 && driftRatio < 0 { + if slowDowns > 2 && driftRatio > 0.05 || slowDowns > 4 && driftRatio < -0.05 { return nil, fmt.Errorf("received too many slow_down responses; detected clock drift of roughly %.0f%% between monotonic and wall clocks; please ensure your system clock is accurate", driftRatio*100) } diff --git a/device/device_flow_test.go b/device/device_flow_test.go index 8be386f..600b4c2 100644 --- a/device/device_flow_test.go +++ b/device/device_flow_test.go @@ -707,7 +707,7 @@ func TestPollToken(t *testing.T) { }, }, { - name: "failure with multiple slow downs, due to high clock drift (mono slower)", + name: "failure with multiple slow downs, due to high clock drift (mono faster)", args: args{ http: apiClient{ stubs: []apiStub{ @@ -815,10 +815,10 @@ func TestPollToken(t *testing.T) { Interval: 5, }, newPoller: singletonFakePoller(6), - calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(-0.01, -0.01, -0.01, -0.01, -0.01), + calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(-0.051, -0.051, -0.051, -0.051, -0.051), }, }, - wantErr: `received too many slow_down responses; detected clock drift of roughly -1% between monotonic and wall clocks; please ensure your system clock is accurate`, + wantErr: `received too many slow_down responses; detected clock drift of roughly -5% between monotonic and wall clocks; please ensure your system clock is accurate`, posts: repeatPostArgs(6, postArgs{ url: "https://github.com/oauth", params: url.Values{ From 09402be2db644e76a42f666dab97a38dc7cd8805 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 28 Jan 2026 13:27:29 +0000 Subject: [PATCH 4/6] fix: bail out on the 3rd slow_down Signed-off-by: Babak K. Shandiz --- device/device_flow.go | 21 ++--- device/device_flow_test.go | 154 ++----------------------------------- 2 files changed, 13 insertions(+), 162 deletions(-) diff --git a/device/device_flow.go b/device/device_flow.go index 6bb84c3..36a3d4f 100644 --- a/device/device_flow.go +++ b/device/device_flow.go @@ -246,19 +246,14 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api if apiError.Code == "slow_down" { slowDowns++ - // See if we can detect a drift between the monotonic and wall clocks. - driftRatio := calculateTimeDriftRatioF(tstart, tstop) - - // A positive drift ratio means the monotonic clock is faster than - // the wall clock. We should avoid hanging here in an endless loop of - // slow-downs. - // - // A negative drift (monotonic clock is slower than the wall clock), - // should not cause slow_down errors, unless the slow monotonic clock - // is still ticking faster than the OAuth server's clock. For such - // cases we tolerate a few more slow-downs. - if slowDowns > 2 && driftRatio > 0.05 || slowDowns > 4 && driftRatio < -0.05 { - return nil, fmt.Errorf("received too many slow_down responses; detected clock drift of roughly %.0f%% between monotonic and wall clocks; please ensure your system clock is accurate", driftRatio*100) + // Since we have already added the secondary safety multiplier upon + // receiving the first slow_down, getting more than 2 is likely an + // indication of a huge clock drift (40% faster mono). More polling + // is just futile unless we apply some unreasonable large multiplier. + // So, we bail out and inform the user about the potential cause. + if slowDowns > 2 { + driftRatio := calculateTimeDriftRatioF(tstart, tstop) + return nil, fmt.Errorf("too many slow_down responses; detected clock drift of roughly %.0f%% between monotonic and wall clocks; please ensure your system clock is accurate", driftRatio*100) } // Based on the RFC spec, we must add 5 seconds to our current polling interval. diff --git a/device/device_flow_test.go b/device/device_flow_test.go index 600b4c2..3d4a0e2 100644 --- a/device/device_flow_test.go +++ b/device/device_flow_test.go @@ -326,14 +326,9 @@ func TestPollToken(t *testing.T) { } } - newCalculateTimeDriftRatioStub := func(driftRatios ...float64) func(tstart, tstop time.Time) float64 { - var count int + newCalculateTimeDriftRatioStub := func(driftRatio float64) func(tstart, tstop time.Time) float64 { return func(_, _ time.Time) float64 { - count++ - if count > len(driftRatios) { - return 0 - } - return driftRatios[count-1] + return driftRatio } } @@ -639,75 +634,7 @@ func TestPollToken(t *testing.T) { }, }, { - name: "success with multiple slow downs, and acceptable monotonic clock drift", - args: args{ - http: apiClient{ - stubs: []apiStub{ - { - body: "error=authorization_pending", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "access_token=123abc", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - }, - }, - url: "https://github.com/oauth", - opts: WaitOptions{ - ClientID: "CLIENT-ID", - DeviceCode: &CodeResponse{ - DeviceCode: "DEVIC", - UserCode: "123-abc", - VerificationURI: "http://verify.me", - ExpiresIn: 99, - Interval: 5, - }, - newPoller: singletonFakePoller(5), - calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.0499, 0.0499, 0.0499), - }, - }, - want: &api.AccessToken{ - Token: "123abc", - }, - posts: repeatPostArgs(5, postArgs{ - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }), - assertFunc: func(t *testing.T, a args) { - // Get the created poller - _, poller := a.opts.newPoller(context.Background(), 0, 0) - got := poller.(*fakePoller).updatedIntervals - want := []time.Duration{10 * time.Second, 15 * time.Second, 20 * time.Second} - if !reflect.DeepEqual(got, want) { - t.Errorf("unexpected updated intervals = %v, want %v", got, want) - } - assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) - }, - }, - { - name: "failure with multiple slow downs, due to high clock drift (mono faster)", + name: "failure with exceeding slow downs", args: args{ http: apiClient{ stubs: []apiStub{ @@ -744,10 +671,10 @@ func TestPollToken(t *testing.T) { Interval: 5, }, newPoller: singletonFakePoller(4), - calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.051, 0.051, 0.051), + calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.10), }, }, - wantErr: `received too many slow_down responses; detected clock drift of roughly 5% between monotonic and wall clocks; please ensure your system clock is accurate`, + wantErr: `too many slow_down responses; detected clock drift of roughly 10% between monotonic and wall clocks; please ensure your system clock is accurate`, posts: repeatPostArgs(4, postArgs{ url: "https://github.com/oauth", params: url.Values{ @@ -767,77 +694,6 @@ func TestPollToken(t *testing.T) { assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, - { - name: "failure with multiple slow downs, due to high clock drift (mono slower)", - args: args{ - http: apiClient{ - stubs: []apiStub{ - { - body: "error=authorization_pending", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - }, - }, - url: "https://github.com/oauth", - opts: WaitOptions{ - ClientID: "CLIENT-ID", - DeviceCode: &CodeResponse{ - DeviceCode: "DEVIC", - UserCode: "123-abc", - VerificationURI: "http://verify.me", - ExpiresIn: 99, - Interval: 5, - }, - newPoller: singletonFakePoller(6), - calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(-0.051, -0.051, -0.051, -0.051, -0.051), - }, - }, - wantErr: `received too many slow_down responses; detected clock drift of roughly -5% between monotonic and wall clocks; please ensure your system clock is accurate`, - posts: repeatPostArgs(6, postArgs{ - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }), - assertFunc: func(t *testing.T, a args) { - // Get the created poller - _, poller := a.opts.newPoller(context.Background(), 0, 0) - got := poller.(*fakePoller).updatedIntervals - want := []time.Duration{10 * time.Second, 15 * time.Second, 20 * time.Second, 25 * time.Second} - if !reflect.DeepEqual(got, want) { - t.Errorf("unexpected updated intervals = %v, want %v", got, want) - } - assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) - }, - }, { name: "with client secret and grant type", args: args{ From 6a15ea962cbb77208dd620897ebe1411a84a6ddd Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 28 Jan 2026 13:28:41 +0000 Subject: [PATCH 5/6] docs: fix comment Signed-off-by: Babak K. Shandiz --- device/device_flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device/device_flow.go b/device/device_flow.go index 36a3d4f..d5fd174 100644 --- a/device/device_flow.go +++ b/device/device_flow.go @@ -249,7 +249,7 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api // Since we have already added the secondary safety multiplier upon // receiving the first slow_down, getting more than 2 is likely an // indication of a huge clock drift (40% faster mono). More polling - // is just futile unless we apply some unreasonable large multiplier. + // is just futile unless we apply some unreasonably large multiplier. // So, we bail out and inform the user about the potential cause. if slowDowns > 2 { driftRatio := calculateTimeDriftRatioF(tstart, tstop) From 21cb7d7fc7d9f8bde205fc627658d52435350286 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 28 Jan 2026 13:40:10 +0000 Subject: [PATCH 6/6] fix: bail out on the 2nd slow_down Signed-off-by: Babak K. Shandiz --- device/device_flow.go | 6 +- device/device_flow_test.go | 137 ++----------------------------------- 2 files changed, 7 insertions(+), 136 deletions(-) diff --git a/device/device_flow.go b/device/device_flow.go index d5fd174..f63a824 100644 --- a/device/device_flow.go +++ b/device/device_flow.go @@ -247,11 +247,11 @@ func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api slowDowns++ // Since we have already added the secondary safety multiplier upon - // receiving the first slow_down, getting more than 2 is likely an - // indication of a huge clock drift (40% faster mono). More polling + // receiving the first slow_down, getting a second one is a strong + // indication of a huge clock drift (+40% faster mono). More polling // is just futile unless we apply some unreasonably large multiplier. // So, we bail out and inform the user about the potential cause. - if slowDowns > 2 { + if slowDowns > 1 { driftRatio := calculateTimeDriftRatioF(tstart, tstop) return nil, fmt.Errorf("too many slow_down responses; detected clock drift of roughly %.0f%% between monotonic and wall clocks; please ensure your system clock is accurate", driftRatio*100) } diff --git a/device/device_flow_test.go b/device/device_flow_test.go index 3d4a0e2..327e288 100644 --- a/device/device_flow_test.go +++ b/device/device_flow_test.go @@ -452,68 +452,6 @@ func TestPollToken(t *testing.T) { assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, - { - name: "success with multiple slow down, new interval in returned response", - args: args{ - http: apiClient{ - stubs: []apiStub{ - { - body: "error=authorization_pending", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down&error_description=Too+many+requests+have+been+made+in+the+same+timeframe.&error_uri=https%3A%2F%2Fdocs.github.com&interval=22", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down&error_description=Too+many+requests+have+been+made+in+the+same+timeframe.&error_uri=https%3A%2F%2Fdocs.github.com&interval=33", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "access_token=123abc", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - }, - }, - url: "https://github.com/oauth", - opts: WaitOptions{ - ClientID: "CLIENT-ID", - DeviceCode: &CodeResponse{ - DeviceCode: "DEVIC", - UserCode: "123-abc", - VerificationURI: "http://verify.me", - ExpiresIn: 99, - Interval: 5, - }, - newPoller: singletonFakePoller(4), - }, - }, - want: &api.AccessToken{ - Token: "123abc", - }, - posts: repeatPostArgs(4, postArgs{ - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }), - assertFunc: func(t *testing.T, a args) { - // Get the created poller - _, poller := a.opts.newPoller(context.Background(), 0, 0) - got := poller.(*fakePoller).updatedIntervals - want := []time.Duration{22 * time.Second, 33 * time.Second} - if !reflect.DeepEqual(got, want) { - t.Errorf("unexpected updated intervals = %v, want %v", got, want) - } - assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) - }, - }, { name: "success with slow down, no interval in returned response", args: args{ @@ -571,68 +509,6 @@ func TestPollToken(t *testing.T) { assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, - { - name: "success with multiple slow down, no interval in returned response", - args: args{ - http: apiClient{ - stubs: []apiStub{ - { - body: "error=authorization_pending", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - { - body: "access_token=123abc", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, - }, - }, - url: "https://github.com/oauth", - opts: WaitOptions{ - ClientID: "CLIENT-ID", - DeviceCode: &CodeResponse{ - DeviceCode: "DEVIC", - UserCode: "123-abc", - VerificationURI: "http://verify.me", - ExpiresIn: 99, - Interval: 5, - }, - newPoller: singletonFakePoller(4), - }, - }, - want: &api.AccessToken{ - Token: "123abc", - }, - posts: repeatPostArgs(4, postArgs{ - url: "https://github.com/oauth", - params: url.Values{ - "client_id": {"CLIENT-ID"}, - "device_code": {"DEVIC"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }, - }), - assertFunc: func(t *testing.T, a args) { - // Get the created poller - _, poller := a.opts.newPoller(context.Background(), 0, 0) - got := poller.(*fakePoller).updatedIntervals - want := []time.Duration{10 * time.Second, 15 * time.Second} - if !reflect.DeepEqual(got, want) { - t.Errorf("unexpected updated intervals = %v, want %v", got, want) - } - assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) - }, - }, { name: "failure with exceeding slow downs", args: args{ @@ -653,11 +529,6 @@ func TestPollToken(t *testing.T) { status: 200, contentType: "application/x-www-form-urlencoded; charset=utf-8", }, - { - body: "error=slow_down", - status: 200, - contentType: "application/x-www-form-urlencoded; charset=utf-8", - }, }, }, url: "https://github.com/oauth", @@ -670,12 +541,12 @@ func TestPollToken(t *testing.T) { ExpiresIn: 99, Interval: 5, }, - newPoller: singletonFakePoller(4), + newPoller: singletonFakePoller(3), calculateTimeDriftRatioF: newCalculateTimeDriftRatioStub(0.10), }, }, wantErr: `too many slow_down responses; detected clock drift of roughly 10% between monotonic and wall clocks; please ensure your system clock is accurate`, - posts: repeatPostArgs(4, postArgs{ + posts: repeatPostArgs(3, postArgs{ url: "https://github.com/oauth", params: url.Values{ "client_id": {"CLIENT-ID"}, @@ -687,11 +558,11 @@ func TestPollToken(t *testing.T) { // Get the created poller _, poller := a.opts.newPoller(context.Background(), 0, 0) got := poller.(*fakePoller).updatedIntervals - want := []time.Duration{10 * time.Second, 15 * time.Second} + want := []time.Duration{10 * time.Second} if !reflect.DeepEqual(got, want) { t.Errorf("unexpected updated intervals = %v, want %v", got, want) } - assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4, 1.4}, poller.(*fakePoller).waitMultipliers) + assertWaitMultipliers(t, []float64{1.2, 1.2, 1.4}, poller.(*fakePoller).waitMultipliers) }, }, {