@@ -29,7 +29,6 @@ type attempt struct {
2929 delay time.Duration
3030 responses []* http.Response
3131 expectedErrorCode int
32- expectedModified bool
3332}
3433
3534func TestLoader_LoadConfig (t * testing.T ) {
@@ -80,7 +79,6 @@ func TestLoader_LoadConfig(t *testing.T) {
8079 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=60" ),
8180 },
8281 expectedErrorCode : 0 , // no error expected
83- expectedModified : true ,
8482 },
8583 },
8684 },
@@ -92,11 +90,9 @@ func TestLoader_LoadConfig(t *testing.T) {
9290 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=60" ),
9391 },
9492 expectedErrorCode : 0 , // no error expected
95- expectedModified : true ,
9693 },
9794 {
9895 expectedErrorCode : 0 , // no error expected
99- expectedModified : false ,
10096 },
10197 },
10298 },
@@ -108,7 +104,6 @@ func TestLoader_LoadConfig(t *testing.T) {
108104 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=60" ),
109105 },
110106 expectedErrorCode : 0 , // no error expected
111- expectedModified : true ,
112107 },
113108 {
114109 delay : 10 * time .Minute ,
@@ -117,19 +112,16 @@ func TestLoader_LoadConfig(t *testing.T) {
117112 newResponse (t , 304 , map [string ]any {}, `"etag-value"` , "max-age=30" ),
118113 },
119114 expectedErrorCode : 0 , // no error expected
120- expectedModified : false ,
121115 },
122116 {
123117 expectedErrorCode : 0 , // no error expected
124- expectedModified : false ,
125118 },
126119 {
127120 delay : 31 * time .Second ,
128121 responses : []* http.Response {
129122 newResponse (t , 304 , map [string ]any {}, `"etag-value"` , "max-age=30" ),
130123 },
131124 expectedErrorCode : 0 , // no error expected
132- expectedModified : false ,
133125 },
134126 },
135127 },
@@ -141,19 +133,16 @@ func TestLoader_LoadConfig(t *testing.T) {
141133 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=60" ),
142134 },
143135 expectedErrorCode : 0 , // no error expected
144- expectedModified : true ,
145136 },
146137 {
147138 delay : 10 * time .Minute ,
148139 responses : []* http.Response {
149140 newResponse (t , 200 , map [string ]any {"upstreamAppUrl" : "http://new-app.local" }, `"etag-new-value"` , "max-age=60" ),
150141 },
151142 expectedErrorCode : 0 , // no error expected
152- expectedModified : true ,
153143 },
154144 {
155145 expectedErrorCode : 0 , // no error expected
156- expectedModified : false ,
157146 },
158147 },
159148 },
@@ -165,7 +154,6 @@ func TestLoader_LoadConfig(t *testing.T) {
165154 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=60" ),
166155 },
167156 expectedErrorCode : 0 , // no error expected
168- expectedModified : true ,
169157 },
170158 {
171159 delay : 10 * time .Minute ,
@@ -178,7 +166,6 @@ func TestLoader_LoadConfig(t *testing.T) {
178166 newResponse (t , 500 , map [string ]any {}, "" , "" ),
179167 },
180168 expectedErrorCode : 0 , // no error expected
181- expectedModified : false ,
182169 },
183170 {
184171 delay : time .Hour ,
@@ -191,7 +178,6 @@ func TestLoader_LoadConfig(t *testing.T) {
191178 newResponse (t , 500 , map [string ]any {}, "" , "" ),
192179 },
193180 expectedErrorCode : 500 ,
194- expectedModified : false ,
195181 },
196182 },
197183 },
@@ -203,20 +189,17 @@ func TestLoader_LoadConfig(t *testing.T) {
203189 newResponse (t , 200 , appPayload , `"etag-value"` , "max-age=7200" ),
204190 },
205191 expectedErrorCode : 0 , // no error expected
206- expectedModified : true ,
207192 },
208193 {
209194 delay : 59 * time .Minute ,
210195 expectedErrorCode : 0 , // no error expected
211- expectedModified : false ,
212196 },
213197 {
214198 delay : 2 * time .Minute ,
215199 responses : []* http.Response {
216200 newResponse (t , 304 , map [string ]any {}, `"etag-value"` , "max-age=30" ),
217201 },
218202 expectedErrorCode : 0 , // no error expected
219- expectedModified : false ,
220203 },
221204 },
222205 },
@@ -244,7 +227,7 @@ func TestLoader_LoadConfig(t *testing.T) {
244227 httpmock .ResponderFromMultipleResponses (attempt .responses , t .Log ),
245228 )
246229
247- cfg , modified , err := loader .GetConfig (ctx , appID )
230+ cfg , err := loader .GetConfig (ctx , appID )
248231 if attempt .expectedErrorCode != 0 {
249232 require .Error (t , err )
250233 var apiErr * api.Error
@@ -255,7 +238,6 @@ func TestLoader_LoadConfig(t *testing.T) {
255238 require .NoError (t , err )
256239 assert .NotEmpty (t , cfg )
257240 }
258- assert .Equal (t , attempt .expectedModified , modified )
259241 assert .Equal (t , len (attempt .responses ), transport .GetTotalCallCount ())
260242 }
261243 })
@@ -293,7 +275,7 @@ func TestLoader_LoadConfig_Race(t *testing.T) {
293275 // Load configuration 10x in parallel
294276 for range 10 {
295277 wg .Go (func () {
296- cfg , _ , err := loader .GetConfig (ctx , appID )
278+ cfg , err := loader .GetConfig (ctx , appID )
297279 require .NoError (t , err )
298280 assert .Equal (t , "http://app.local" , cfg .UpstreamAppURL )
299281 counter .Add (1 )
@@ -307,6 +289,49 @@ func TestLoader_LoadConfig_Race(t *testing.T) {
307289 assert .Equal (t , int64 (10 ), counter .Load ())
308290}
309291
292+ // TestLoader_GetConfig_RefreshSurvivesRequestCancellation verifies that the config
293+ // refresh is not aborted when the originating request's context is canceled (e.g. the
294+ // browser disconnects during an OAuth redirect). The refresh must run on a context
295+ // detached from the request so the shared cache is still updated with the new config.
296+ func TestLoader_GetConfig_RefreshSurvivesRequestCancellation (t * testing.T ) {
297+ t .Parallel ()
298+
299+ appID := api .AppID ("test" )
300+ appPayload := map [string ]any {
301+ "appId" : appID .String (),
302+ "appName" : "my-test" ,
303+ "projectId" : "123" ,
304+ "upstreamAppUrl" : "http://app.local" ,
305+ }
306+
307+ clk := clockwork .NewFakeClock ()
308+ d , mock := dependencies .NewMockedServiceScope (t , t .Context (), config .New (), commonDeps .WithClock (clk ))
309+
310+ transport := mock .MockedHTTPTransport ()
311+ transport .RegisterResponder (
312+ http .MethodGet ,
313+ fmt .Sprintf ("%s/apps/%s/proxy-config" , mock .TestConfig ().SandboxesAPI .URL , appID ),
314+ // Fail if the request context is canceled; succeed otherwise. This detects
315+ // whether the refresh runs on the (canceled) request context or a detached one.
316+ func (req * http.Request ) (* http.Response , error ) {
317+ if err := req .Context ().Err (); err != nil {
318+ return nil , err
319+ }
320+ return newResponse (t , http .StatusOK , appPayload , `"etag-value"` , "max-age=60" ), nil
321+ },
322+ )
323+ loader := d .AppConfigLoader ()
324+
325+ // Simulate a client that disconnected before the refresh completes.
326+ reqCtx , cancel := context .WithCancelCause (t .Context ())
327+ cancel (context .Canceled )
328+
329+ cfg , err := loader .GetConfig (reqCtx , appID )
330+ require .NoError (t , err )
331+ assert .Equal (t , "http://app.local" , cfg .UpstreamAppURL )
332+ assert .Equal (t , 1 , transport .GetTotalCallCount ())
333+ }
334+
310335func newResponse (t * testing.T , code int , body map [string ]any , eTag string , cacheControl string ) * http.Response {
311336 t .Helper ()
312337
0 commit comments