@@ -284,12 +284,16 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
284284 if redirectURL .Path != "/settings/slack/install/select" {
285285 t .Fatalf ("expected React picker route, got %q" , redirectURL .Path )
286286 }
287- if redirectURL .RawQuery != "" {
287+ requestID := redirectURL .Query ().Get ("requestId" )
288+ if requestID == "" {
289+ t .Fatalf ("expected picker redirect to include requestId, got %q" , redirectURL .RawQuery )
290+ }
291+ if strings .Contains (redirectURL .RawQuery , "state=" ) {
288292 t .Fatalf ("expected picker redirect without pending state query, got %q" , redirectURL .RawQuery )
289293 }
290294 var pendingCookie * http.Cookie
291295 for _ , cookie := range rec .Result ().Cookies () {
292- if cookie .Name == pendingInstallCookieName {
296+ if cookie .Name == pendingInstallCookieNameForRequest ( requestID ) {
293297 pendingCookie = cookie
294298 break
295299 }
@@ -309,7 +313,7 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
309313
310314 selectionReq := httptest .NewRequest (
311315 http .MethodGet ,
312- "/api/slack/install/selection" ,
316+ "/api/slack/install/selection?requestId=" + url . QueryEscape ( requestID ) ,
313317 nil ,
314318 )
315319 selectionReq .AddCookie (pendingCookie )
@@ -331,6 +335,90 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
331335 }
332336}
333337
338+ func TestInstallTargetSelectionAPIUsesRequestScopedPendingCookie (t * testing.T ) {
339+ backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
340+ if r .URL .Path != "/internal/v2/spritz/channel-install-targets/list" {
341+ t .Fatalf ("unexpected backend path %s" , r .URL .Path )
342+ }
343+ writeJSON (w , http .StatusOK , map [string ]any {
344+ "status" : "resolved" ,
345+ "targets" : []map [string ]any {
346+ {
347+ "id" : "ag_workspace" ,
348+ "profile" : map [string ]any {
349+ "name" : "Workspace Helper" ,
350+ },
351+ "presetInputs" : map [string ]any {
352+ "agentId" : "ag_workspace" ,
353+ },
354+ },
355+ },
356+ })
357+ }))
358+ defer backend .Close ()
359+
360+ gateway := newSlackGateway (config {
361+ BackendBaseURL : backend .URL ,
362+ BackendFastAPIBaseURL : backend .URL ,
363+ BackendInternalToken : "backend-internal-token" ,
364+ OAuthStateSecret : "oauth-state-secret" ,
365+ PrincipalID : "shared-slack-gateway" ,
366+ HTTPTimeout : 5 * time .Second ,
367+ }, slog .New (slog .NewTextHandler (io .Discard , nil )))
368+
369+ pendingStateA , err := gateway .state .generatePendingInstall (pendingInstallState {
370+ RequestID : "install-request-a" ,
371+ Installation : slackInstallation {
372+ TeamID : "T_workspace_a" ,
373+ InstallingUserID : "U_installer" ,
374+ BotAccessToken : "xoxb-installed-a" ,
375+ BotUserID : "U_bot" ,
376+ APIAppID : "A_app_1" ,
377+ },
378+ })
379+ if err != nil {
380+ t .Fatalf ("generate first pending install state failed: %v" , err )
381+ }
382+ pendingStateB , err := gateway .state .generatePendingInstall (pendingInstallState {
383+ RequestID : "install-request-b" ,
384+ Installation : slackInstallation {
385+ TeamID : "T_workspace_b" ,
386+ InstallingUserID : "U_installer" ,
387+ BotAccessToken : "xoxb-installed-b" ,
388+ BotUserID : "U_bot" ,
389+ APIAppID : "A_app_1" ,
390+ },
391+ })
392+ if err != nil {
393+ t .Fatalf ("generate second pending install state failed: %v" , err )
394+ }
395+
396+ req := httptest .NewRequest (http .MethodGet , "/api/slack/install/selection?requestId=install-request-a" , nil )
397+ req .AddCookie (& http.Cookie {
398+ Name : pendingInstallCookieNameForRequest ("install-request-a" ),
399+ Value : pendingStateA ,
400+ Path : "/api/slack/install/selection" ,
401+ })
402+ req .AddCookie (& http.Cookie {
403+ Name : pendingInstallCookieNameForRequest ("install-request-b" ),
404+ Value : pendingStateB ,
405+ Path : "/api/slack/install/selection" ,
406+ })
407+ rec := httptest .NewRecorder ()
408+ gateway .routes ().ServeHTTP (rec , req )
409+
410+ if rec .Code != http .StatusOK {
411+ t .Fatalf ("expected 200, got %d: %s" , rec .Code , rec .Body .String ())
412+ }
413+ var payload map [string ]any
414+ if err := json .NewDecoder (rec .Body ).Decode (& payload ); err != nil {
415+ t .Fatalf ("decode selection payload: %v" , err )
416+ }
417+ if payload ["requestId" ] != "install-request-a" || payload ["teamId" ] != "T_workspace_a" {
418+ t .Fatalf ("expected first pending install, got %#v" , payload )
419+ }
420+ }
421+
334422func TestWorkspaceManagementRequiresBrowserPrincipal (t * testing.T ) {
335423 gateway := newSlackGateway (config {
336424 BackendFastAPIBaseURL : "https://backend.example.test" ,
@@ -779,6 +867,67 @@ func TestChannelSettingsAPIUpdatePostsRoutePolicies(t *testing.T) {
779867 }
780868}
781869
870+ func TestChannelSettingsAPIMissingConnectionReturnsJSON (t * testing.T ) {
871+ backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
872+ if r .URL .Path != "/internal/v2/spritz/channel-installations/list" {
873+ t .Fatalf ("unexpected backend path %s" , r .URL .Path )
874+ }
875+ writeJSON (w , http .StatusOK , map [string ]any {
876+ "status" : "resolved" ,
877+ "installations" : []map [string ]any {
878+ {
879+ "id" : "chinst_example" ,
880+ "route" : map [string ]any {
881+ "principalId" : "shared-slack-gateway" ,
882+ "provider" : "slack" ,
883+ "externalScopeType" : "workspace" ,
884+ "externalTenantId" : "T_workspace_1" ,
885+ },
886+ "state" : "ready" ,
887+ "connections" : []map [string ]any {
888+ {
889+ "id" : "chconn_example" ,
890+ "isDefault" : true ,
891+ "state" : "ready" ,
892+ },
893+ },
894+ },
895+ },
896+ })
897+ }))
898+ defer backend .Close ()
899+
900+ gateway := newSlackGateway (config {
901+ BackendFastAPIBaseURL : backend .URL ,
902+ BackendInternalToken : "backend-internal-token" ,
903+ PrincipalID : "shared-slack-gateway" ,
904+ HTTPTimeout : 5 * time .Second ,
905+ }, slog .New (slog .NewTextHandler (io .Discard , nil )))
906+
907+ req := httptest .NewRequest (
908+ http .MethodGet ,
909+ "/api/settings/channels/installations/chinst_example/connections/missing" ,
910+ nil ,
911+ )
912+ req .Header .Set ("X-Spritz-User-Id" , "user-1" )
913+ rec := httptest .NewRecorder ()
914+ gateway .routes ().ServeHTTP (rec , req )
915+
916+ if rec .Code != http .StatusNotFound {
917+ t .Fatalf ("expected 404, got %d: %s" , rec .Code , rec .Body .String ())
918+ }
919+ if contentType := rec .Header ().Get ("Content-Type" ); ! strings .Contains (contentType , "application/json" ) {
920+ t .Fatalf ("expected JSON content type, got %q" , contentType )
921+ }
922+ var payload map [string ]any
923+ if err := json .NewDecoder (rec .Body ).Decode (& payload ); err != nil {
924+ t .Fatalf ("decode error payload: %v" , err )
925+ }
926+ if payload ["status" ] != "error" || payload ["message" ] != "connection not found" {
927+ t .Fatalf ("expected structured missing connection error, got %#v" , payload )
928+ }
929+ }
930+
782931func TestWorkspaceManagementAcceptsConfiguredBrowserAuthHeaders (t * testing.T ) {
783932 t .Setenv ("SPRITZ_AUTH_HEADER_ID" , "X-Forwarded-User" )
784933 t .Setenv ("SPRITZ_AUTH_HEADER_EMAIL" , "X-Forwarded-Email" )
@@ -1512,7 +1661,7 @@ func TestInstallTargetSelectionAPIPreservesClassifiedUpsertFailure(t *testing.T)
15121661 req := httptest .NewRequest (http .MethodPost , "/api/slack/install/selection" , bytes .NewReader (requestBody ))
15131662 req .Header .Set ("Content-Type" , "application/json" )
15141663 req .AddCookie (& http.Cookie {
1515- Name : pendingInstallCookieName ,
1664+ Name : pendingInstallCookieNameForRequest ( "install-request-1" ) ,
15161665 Value : pendingState ,
15171666 Path : "/api/slack/install/selection" ,
15181667 })
@@ -1597,7 +1746,7 @@ func TestInstallTargetSelectionAPIRejectsStaleRequestID(t *testing.T) {
15971746 req := httptest .NewRequest (http .MethodPost , "/api/slack/install/selection" , bytes .NewReader (requestBody ))
15981747 req .Header .Set ("Content-Type" , "application/json" )
15991748 req .AddCookie (& http.Cookie {
1600- Name : pendingInstallCookieName ,
1749+ Name : pendingInstallCookieNameForRequest ( "install-request-current" ) ,
16011750 Value : pendingState ,
16021751 Path : "/api/slack/install/selection" ,
16031752 })
0 commit comments