77 "net/http"
88 "net/http/httptest"
99 "sync"
10+ "sync/atomic"
1011 "testing"
12+ "testing/synctest"
1113 "time"
1214
1315 "github.com/stretchr/testify/assert"
@@ -66,26 +68,92 @@ func TestTheCredentialWatchAsksAboutAnUnauthorizedRead(t *testing.T) {
6668// A 401 Basecamp does not confirm, from a proxy say, leaves the run going,
6769// and the next one asks again.
6870func TestTheCredentialWatchRunsOnWhenBasecampStillTakesTheCredential (t * testing.T ) {
71+ synctest .Test (t , func (t * testing.T ) {
72+ w := newCredentialWatch ()
73+ transport := w .WrapTransport (statusTransport (http .StatusUnauthorized ))
74+ ctx , cancel := context .WithCancel (t .Context ())
75+ defer cancel ()
76+ var asks atomic.Int32
77+ done := make (chan error , 1 )
78+ go func () {
79+ done <- w .Run (ctx , func (context.Context ) bool { asks .Add (1 ); return false })
80+ }()
81+ for attempt := range int32 (2 ) {
82+ roundTrip (t , transport )
83+ synctest .Wait () // The previous confirmation has finished, not just started.
84+ assert .Equal (t , attempt + 1 , asks .Load ())
85+ }
86+ cancel ()
87+ require .NoError (t , <- done , "ended by its context, the watch stops nothing" )
88+ })
89+ }
90+
91+ func TestARefusedRenewalInterruptsAPendingConfirmation (t * testing.T ) {
6992 w := newCredentialWatch ()
70- transport := w .WrapTransport (statusTransport (http .StatusUnauthorized ))
7193 ctx , cancel := context .WithCancel (t .Context ())
7294 defer cancel ()
73- asks := make (chan struct {})
95+ started , stopped := make ( chan struct {}), make (chan struct {})
7496 done := make (chan error , 1 )
7597 go func () {
76- done <- w .Run (ctx , func (context.Context ) bool { asks <- struct {}{}; return false })
98+ done <- w .Run (ctx , func (ctx context.Context ) bool {
99+ close (started )
100+ <- ctx .Done ()
101+ close (stopped )
102+ return false
103+ })
77104 }()
105+ w .unauthorized ()
106+ select {
107+ case <- started :
108+ case <- time .After (5 * time .Second ):
109+ t .Fatal ("confirmation never started" )
110+ }
111+ w .refuse (auth .ErrLoginRefused )
112+ select {
113+ case err := <- done :
114+ require .ErrorIs (t , err , auth .ErrLoginRefused )
115+ case <- time .After (5 * time .Second ):
116+ t .Fatal ("a definitive refusal waited behind confirmation" )
117+ }
118+ select {
119+ case <- stopped :
120+ case <- time .After (5 * time .Second ):
121+ t .Fatal ("the in-flight confirmation was not canceled" )
122+ }
123+ }
78124
79- for range 2 {
80- roundTrip (t , transport )
81- select {
82- case <- asks :
83- case <- time .After (10 * time .Second ):
84- t .Fatal ("the watch did not ask about a 401" )
125+ func TestUnauthorizedReadsShareOnePendingConfirmation (t * testing.T ) {
126+ synctest .Test (t , func (t * testing.T ) {
127+ w := newCredentialWatch ()
128+ ctx , cancel := context .WithCancel (t .Context ())
129+ defer cancel ()
130+ release := make (chan struct {})
131+ var calls atomic.Int32
132+ done := make (chan error , 1 )
133+ go func () {
134+ done <- w .Run (ctx , func (context.Context ) bool {
135+ calls .Add (1 )
136+ <- release
137+ return false
138+ })
139+ }()
140+ w .unauthorized ()
141+ synctest .Wait ()
142+ require .EqualValues (t , 1 , calls .Load ())
143+ for range 20 {
144+ w .unauthorized ()
85145 }
86- }
87- cancel ()
88- require .NoError (t , <- done , "ended by its context, the watch stops nothing" )
146+ synctest .Wait ()
147+ assert .EqualValues (t , 1 , calls .Load ())
148+ close (release )
149+ synctest .Wait ()
150+ assert .EqualValues (t , 1 , calls .Load (), "pending suspicions share the completed answer" )
151+ w .unauthorized ()
152+ synctest .Wait ()
153+ assert .EqualValues (t , 2 , calls .Load (), "a later refusal is checked again" )
154+ cancel ()
155+ require .NoError (t , <- done )
156+ })
89157}
90158
91159// A bot user's refresh the token endpoint refuses is Basecamp's answer
@@ -167,6 +235,30 @@ func TestABotUsersRefreshThatCanPassDoesNotStopTheRun(t *testing.T) {
167235 }
168236}
169237
238+ func TestRepeatedUnauthorizedReadsDuringARefreshHoldDoNotSignOut (t * testing.T ) {
239+ bc := newBotBasecamp (t )
240+ bc .answerRefresh (http .StatusTooManyRequests , `{"error":"temporarily_unavailable"}` )
241+ bc .answerMe (func (string ) int { return http .StatusUnauthorized })
242+ app := bc .app (t )
243+ for range 2 {
244+ assert .False (t , confirmCredentialRefused (t .Context (), app , setup .KindBotUser , "bot" ))
245+ }
246+ assert .Equal (t , []string {"refresh" }, bc .asked (), "a held refresh must not probe the old token or refresh twice" )
247+ }
248+
249+ func TestALocalRefreshPreflightFailureDoesNotProveRevocation (t * testing.T ) {
250+ bc := newBotBasecamp (t )
251+ bc .answerMe (func (string ) int { return http .StatusUnauthorized })
252+ app := bc .app (t )
253+ creds , err := app .Auth .GetStore ().Load ("profile:bot" )
254+ require .NoError (t , err )
255+ creds .TokenEndpoint = "ftp://invalid.example/tokens"
256+ require .Error (t , app .Auth .RefreshRefusal (creds ))
257+ require .NoError (t , app .Auth .GetStore ().Save ("profile:bot" , creds ))
258+ assert .False (t , confirmCredentialRefused (t .Context (), app , setup .KindBotUser , "bot" ))
259+ assert .Empty (t , bc .asked (), "a configuration fault is not proof from the old token" )
260+ }
261+
170262// A bot login stored from a bare token has no refresh to try; the token it
171263// has is the only one it will ever have, so a 401 for it is the answer.
172264func TestABotLoginWithNoRefreshIsAskedAboutWithItsOwnToken (t * testing.T ) {
@@ -182,6 +274,18 @@ func TestABotLoginWithNoRefreshIsAskedAboutWithItsOwnToken(t *testing.T) {
182274 assert .Equal (t , []string {"me old-tok" }, bc .asked ())
183275}
184276
277+ func TestABoostRetainsARefusedRenewalForTheCredentialWatch (t * testing.T ) {
278+ w := newCredentialWatch ()
279+ tokens := w .Tokens (tokenFunc (func () (string , error ) { return "" , refusedLogin () }))
280+ client := basecamp .NewClient (& basecamp.Config {BaseURL : "http://127.0.0.1:1" }, tokens ).ForAccount ("555" )
281+ booster := connector.SDKBooster {Client : client }
282+ _ , err := booster .Boost (t .Context (), 123 , "ok" )
283+ require .Error (t , err )
284+ // Even if acknowledgement only logs the failure, the definitive
285+ // refusal remains visible to the connector's credential watch.
286+ assert .ErrorIs (t , runWatch (t , w , mustNotConfirm (t )), auth .ErrLoginRefused )
287+ }
288+
185289// Every other failure is the reads' own: neither a renewal that failed for
186290// another reason nor another status asks Basecamp or stops the run.
187291func TestTheCredentialWatchIgnoresOtherFailures (t * testing.T ) {
0 commit comments