@@ -137,11 +137,80 @@ func TestLogin(t *testing.T) {
137137 t .Errorf ("got output %q, want %q" , gotOut , wantOut )
138138 }
139139 })
140+
141+ t .Run ("invalid stored oauth token restarts device flow" , func (t * testing.T ) {
142+ var authHeaders []string
143+ s := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
144+ authHeaders = append (authHeaders , r .Header .Get ("Authorization" ))
145+ if r .Header .Get ("Authorization" ) != "Bearer new-oauth-token" {
146+ http .Error (w , "" , http .StatusUnauthorized )
147+ return
148+ }
149+ fmt .Fprintln (w , `{"data":{"currentUser":{"username":"alice"}}}` )
150+ }))
151+ defer s .Close ()
152+
153+ restoreStoredOAuthLoader (t , func (_ context.Context , _ * url.URL ) (* oauth.Token , error ) {
154+ return & oauth.Token {
155+ Endpoint : s .URL ,
156+ ClientID : oauth .DefaultClientID ,
157+ AccessToken : "old-oauth-token" ,
158+ ExpiresAt : time .Now ().Add (time .Hour ),
159+ }, nil
160+ })
161+ restoreOAuthTokenStore (t , func (context.Context , * oauth.Token ) error { return nil })
162+
163+ u , _ := url .ParseRequestURI (s .URL )
164+ startCalled := false
165+ pollCalled := false
166+ var out bytes.Buffer
167+ err := loginCmd (context .Background (), loginParams {
168+ cfg : & config {endpointURL : u },
169+ client : (& config {endpointURL : u }).apiClient (nil , io .Discard ),
170+ out : & out ,
171+ oauthClient : fakeOAuthClient {
172+ startCalled : & startCalled ,
173+ deviceResp : & oauth.DeviceAuthResponse {
174+ DeviceCode : "device-code" ,
175+ ExpiresIn : 60 ,
176+ },
177+ pollCalled : & pollCalled ,
178+ pollResp : & oauth.TokenResponse {
179+ AccessToken : "new-oauth-token" ,
180+ ExpiresIn : 3600 ,
181+ TokenType : "Bearer" ,
182+ },
183+ },
184+ })
185+ if err != nil {
186+ t .Fatal (err )
187+ }
188+ if ! startCalled || ! pollCalled {
189+ t .Fatal ("expected invalid stored oauth token to restart device flow" )
190+ }
191+ if len (authHeaders ) != 2 || authHeaders [0 ] != "Bearer old-oauth-token" || authHeaders [1 ] != "Bearer new-oauth-token" {
192+ t .Fatalf ("Authorization headers = %q, want old token then new token" , authHeaders )
193+ }
194+ gotOut := out .String ()
195+ for _ , want := range []string {
196+ "⚠️ Warning: Stored OAuth credentials could not be verified. Starting a new OAuth device flow." ,
197+ "Waiting for authorization... DONE" ,
198+ "✔︎ Authenticated as alice on " + s .URL ,
199+ "✔︎ Authenticated with OAuth credentials" ,
200+ } {
201+ if ! strings .Contains (gotOut , want ) {
202+ t .Errorf ("got output %q, want it to contain %q" , gotOut , want )
203+ }
204+ }
205+ })
140206}
141207
142208type fakeOAuthClient struct {
143209 startErr error
144210 startCalled * bool
211+ deviceResp * oauth.DeviceAuthResponse
212+ pollCalled * bool
213+ pollResp * oauth.TokenResponse
145214}
146215
147216func (f fakeOAuthClient ) ClientID () string {
@@ -156,10 +225,22 @@ func (f fakeOAuthClient) Start(context.Context, *url.URL, []string) (*oauth.Devi
156225 if f .startCalled != nil {
157226 * f .startCalled = true
158227 }
159- return nil , f .startErr
228+ if f .startErr != nil {
229+ return nil , f .startErr
230+ }
231+ if f .deviceResp != nil {
232+ return f .deviceResp , nil
233+ }
234+ return nil , fmt .Errorf ("unexpected call to Start" )
160235}
161236
162237func (f fakeOAuthClient ) Poll (context.Context , * url.URL , string , time.Duration , int ) (* oauth.TokenResponse , error ) {
238+ if f .pollCalled != nil {
239+ * f .pollCalled = true
240+ }
241+ if f .pollResp != nil {
242+ return f .pollResp , nil
243+ }
163244 return nil , fmt .Errorf ("unexpected call to Poll" )
164245}
165246
@@ -242,3 +323,13 @@ func restoreStoredOAuthLoader(t *testing.T, loader func(context.Context, *url.UR
242323 loadStoredOAuthToken = prev
243324 })
244325}
326+
327+ func restoreOAuthTokenStore (t * testing.T , store func (context.Context , * oauth.Token ) error ) {
328+ t .Helper ()
329+
330+ prev := storeOAuthToken
331+ storeOAuthToken = store
332+ t .Cleanup (func () {
333+ storeOAuthToken = prev
334+ })
335+ }
0 commit comments