@@ -32,6 +32,24 @@ function waitFor(condition, timeoutMs = 10_000, intervalMs = 50, onTimeoutMessag
3232 } ) ;
3333}
3434
35+ async function reserveFreePort ( ) {
36+ const server = createServer ( ( _req , res ) => {
37+ res . writeHead ( 204 ) ;
38+ res . end ( ) ;
39+ } ) ;
40+
41+ await new Promise ( ( resolve ) => server . listen ( 0 , "127.0.0.1" , resolve ) ) ;
42+ const address = server . address ( ) ;
43+ if ( ! address || typeof address === "string" ) {
44+ await new Promise ( ( resolve ) => server . close ( ( ) => resolve ( undefined ) ) ) ;
45+ throw new Error ( "failed to reserve free port" ) ;
46+ }
47+
48+ const port = address . port ;
49+ await new Promise ( ( resolve ) => server . close ( ( ) => resolve ( undefined ) ) ) ;
50+ return port ;
51+ }
52+
3553describe ( "broker pull bridge semi-integration" , ( ) => {
3654 const children = [ ] ;
3755 const servers = [ ] ;
@@ -621,4 +639,139 @@ describe("broker pull bridge semi-integration", () => {
621639
622640 bridge . kill ( "SIGTERM" ) ;
623641 } ) ;
642+
643+ it ( "sends broker bearer token when configured" , async ( ) => {
644+ await sodium . ready ;
645+
646+ const workspaceId = "T123BROKER" ;
647+ const bridgeApiPort = await reserveFreePort ( ) ;
648+ let outboundAuthorization = null ;
649+
650+ const broker = createServer ( async ( req , res ) => {
651+ if ( req . method === "POST" && req . url === "/api/inbox/pull" ) {
652+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
653+ res . end ( JSON . stringify ( { ok : true , messages : [ ] } ) ) ;
654+ return ;
655+ }
656+
657+ if ( req . method === "POST" && req . url === "/api/send" ) {
658+ outboundAuthorization = req . headers . authorization || null ;
659+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
660+ res . end ( JSON . stringify ( { ok : true , ts : "1234.5678" } ) ) ;
661+ return ;
662+ }
663+
664+ if ( req . method === "POST" && req . url === "/api/inbox/ack" ) {
665+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
666+ res . end ( JSON . stringify ( { ok : true , acked : 0 } ) ) ;
667+ return ;
668+ }
669+
670+ res . writeHead ( 404 , { "Content-Type" : "application/json" } ) ;
671+ res . end ( JSON . stringify ( { ok : false , error : "not found" } ) ) ;
672+ } ) ;
673+
674+ await new Promise ( ( resolve ) => broker . listen ( 0 , "127.0.0.1" , resolve ) ) ;
675+ servers . push ( broker ) ;
676+
677+ const address = broker . address ( ) ;
678+ if ( ! address || typeof address === "string" ) {
679+ throw new Error ( "failed to get broker test server address" ) ;
680+ }
681+ const brokerUrl = `http://127.0.0.1:${ address . port } ` ;
682+
683+ const testFileDir = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
684+ const repoRoot = path . dirname ( testFileDir ) ;
685+ const bridgePath = path . join ( repoRoot , "slack-bridge" , "broker-bridge.mjs" ) ;
686+ const bridgeCwd = path . join ( repoRoot , "slack-bridge" ) ;
687+
688+ const bridge = spawn ( "node" , [ bridgePath ] , {
689+ cwd : bridgeCwd ,
690+ env : {
691+ ...process . env ,
692+ SLACK_BROKER_URL : brokerUrl ,
693+ SLACK_BROKER_WORKSPACE_ID : workspaceId ,
694+ SLACK_BROKER_SERVER_PRIVATE_KEY : b64 ( 32 , 11 ) ,
695+ SLACK_BROKER_SERVER_PUBLIC_KEY : b64 ( 32 , 12 ) ,
696+ SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY : Buffer . alloc ( 32 , 24 ) . toString ( "base64" ) ,
697+ SLACK_BROKER_PUBLIC_KEY : b64 ( 32 , 14 ) ,
698+ SLACK_BROKER_SIGNING_PUBLIC_KEY : b64 ( 32 , 15 ) ,
699+ SLACK_BROKER_ACCESS_TOKEN : "test-broker-token" ,
700+ SLACK_ALLOWED_USERS : "U_ALLOWED" ,
701+ SLACK_BROKER_POLL_INTERVAL_MS : "50" ,
702+ SLACK_BROKER_WAIT_SECONDS : "0" ,
703+ BRIDGE_API_PORT : String ( bridgeApiPort ) ,
704+ } ,
705+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
706+ } ) ;
707+ children . push ( bridge ) ;
708+
709+ const start = Date . now ( ) ;
710+ // Bridge local API may not be ready immediately after spawn; retry until it accepts /send.
711+ while ( Date . now ( ) - start < 10_000 ) {
712+ try {
713+ const res = await fetch ( `http://127.0.0.1:${ bridgeApiPort } /send` , {
714+ method : "POST" ,
715+ headers : { "Content-Type" : "application/json" } ,
716+ body : JSON . stringify ( { channel : "C123" , text : "hello" } ) ,
717+ } ) ;
718+ if ( res . ok ) break ;
719+ } catch {
720+ // retry while bridge boots
721+ }
722+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
723+ }
724+
725+ await waitFor ( ( ) => outboundAuthorization !== null , 10_000 , 50 , "timeout waiting for broker /api/send call" ) ;
726+ expect ( outboundAuthorization ) . toBe ( "Bearer test-broker-token" ) ;
727+
728+ bridge . kill ( "SIGTERM" ) ;
729+ } ) ;
730+
731+ it ( "exits when broker access token is expired" , async ( ) => {
732+ await sodium . ready ;
733+
734+ const testFileDir = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
735+ const repoRoot = path . dirname ( testFileDir ) ;
736+ const bridgePath = path . join ( repoRoot , "slack-bridge" , "broker-bridge.mjs" ) ;
737+ const bridgeCwd = path . join ( repoRoot , "slack-bridge" ) ;
738+
739+ let bridgeStdout = "" ;
740+ let bridgeStderr = "" ;
741+
742+ const bridge = spawn ( "node" , [ bridgePath ] , {
743+ cwd : bridgeCwd ,
744+ env : {
745+ ...process . env ,
746+ SLACK_BROKER_URL : "http://127.0.0.1:65535" ,
747+ SLACK_BROKER_WORKSPACE_ID : "T123BROKER" ,
748+ SLACK_BROKER_SERVER_PRIVATE_KEY : b64 ( 32 , 11 ) ,
749+ SLACK_BROKER_SERVER_PUBLIC_KEY : b64 ( 32 , 12 ) ,
750+ SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY : Buffer . alloc ( 32 , 25 ) . toString ( "base64" ) ,
751+ SLACK_BROKER_PUBLIC_KEY : b64 ( 32 , 14 ) ,
752+ SLACK_BROKER_SIGNING_PUBLIC_KEY : b64 ( 32 , 15 ) ,
753+ SLACK_BROKER_ACCESS_TOKEN : "expired-token" ,
754+ SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT : "2000-01-01T00:00:00.000Z" ,
755+ SLACK_ALLOWED_USERS : "U_ALLOWED" ,
756+ BRIDGE_API_PORT : "0" ,
757+ } ,
758+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
759+ } ) ;
760+
761+ bridge . stdout . on ( "data" , ( chunk ) => {
762+ bridgeStdout += chunk . toString ( ) ;
763+ } ) ;
764+ bridge . stderr . on ( "data" , ( chunk ) => {
765+ bridgeStderr += chunk . toString ( ) ;
766+ } ) ;
767+
768+ children . push ( bridge ) ;
769+
770+ const exited = await new Promise ( ( resolve ) => {
771+ bridge . on ( "exit" , ( code , signal ) => resolve ( { code, signal } ) ) ;
772+ } ) ;
773+
774+ expect ( exited . code ) . toBe ( 1 ) ;
775+ expect ( `${ bridgeStdout } \n${ bridgeStderr } ` ) . toContain ( "broker access token is expired" ) ;
776+ } ) ;
624777} ) ;
0 commit comments