@@ -86,7 +86,7 @@ pub async fn ensure_gateway(
8686 let ( api_id, api_endpoint) = ensure_api ( & api) . await ?;
8787
8888 // ── Integration: VPC Link → Cloud Map DNS name on the container port ────
89- let integration_uri = format ! ( "http://{}:{}" , dns_name, ingress. container_port) ;
89+ let integration_uri = integration_uri ( dns_name, ingress. container_port ) ;
9090 let integration_id = ensure_integration ( & api, & api_id, & vpc_link_id, & integration_uri) . await ?;
9191
9292 // ── One route per webhook path, all → the same integration ─────────────
@@ -97,13 +97,27 @@ pub async fn ensure_gateway(
9797 // ── Stage (auto-deploy) ────────────────────────────────────────────────
9898 ensure_stage ( & api, & api_id) . await ?;
9999
100+ Ok ( webhook_urls ( & api_endpoint, & ingress. paths ) )
101+ }
102+
103+ /// Integration URI for the VPC Link → Cloud Map target on the container port.
104+ fn integration_uri ( dns_name : & str , port : u16 ) -> String {
105+ format ! ( "http://{dns_name}:{port}" )
106+ }
107+
108+ /// API Gateway route key for a webhook path (POST only).
109+ fn route_key ( path : & str ) -> String {
110+ format ! ( "POST {path}" )
111+ }
112+
113+ /// Build the public webhook URL(s) from the API endpoint and paths.
114+ /// Each URL is `<endpoint>/<stage><path>`.
115+ fn webhook_urls ( api_endpoint : & str , paths : & [ String ] ) -> Vec < String > {
100116 let base = api_endpoint. trim_end_matches ( '/' ) ;
101- let urls = ingress
102- . paths
117+ paths
103118 . iter ( )
104119 . map ( |p| format ! ( "{base}/{STAGE_NAME}{p}" ) )
105- . collect ( ) ;
106- Ok ( urls)
120+ . collect ( )
107121}
108122
109123// ─── VPC resolution ─────────────────────────────────────────────────────────
@@ -328,6 +342,13 @@ async fn ensure_vpc_link(
328342
329343 let link_id = if let Some ( ( id, _status) ) = found {
330344 eprintln ! ( " ✓ VPC Link exists: {VPC_LINK_NAME} ({id})" ) ;
345+ // A VPC Link's subnets/SGs are fixed at creation and cannot be updated.
346+ // All ingress-enabled bots in a VPC share this one link, so they must use
347+ // the same subnet/SG set as whichever bot created it first — otherwise the
348+ // link's ENIs won't cover this task's subnets and integrations may 503.
349+ eprintln ! (
350+ " ↳ reusing shared link's subnets/SGs (fixed at creation); ensure all\n ingress bots in this VPC use the same subnets/securityGroups"
351+ ) ;
331352 id
332353 } else {
333354 eprintln ! ( " ⊕ Creating VPC Link: {VPC_LINK_NAME}" ) ;
@@ -437,7 +458,7 @@ async fn ensure_route(
437458 path : & str ,
438459 integration_id : & str ,
439460) -> Result < ( ) > {
440- let route_key = format ! ( "POST { path}" ) ;
461+ let route_key = route_key ( path) ;
441462 let target = format ! ( "integrations/{integration_id}" ) ;
442463 let existing = api
443464 . get_routes ( )
@@ -485,3 +506,49 @@ async fn ensure_stage(api: &aws_sdk_apigatewayv2::Client, api_id: &str) -> Resul
485506 eprintln ! ( " ⊕ Created stage: {STAGE_NAME} (auto-deploy)" ) ;
486507 Ok ( ( ) )
487508}
509+
510+ #[ cfg( test) ]
511+ mod tests {
512+ use super :: * ;
513+
514+ #[ test]
515+ fn integration_uri_uses_dns_and_port ( ) {
516+ assert_eq ! (
517+ integration_uri( "oab-prod-mybot.oab" , 8080 ) ,
518+ "http://oab-prod-mybot.oab:8080"
519+ ) ;
520+ assert_eq ! (
521+ integration_uri( "svc.ns" , 3000 ) ,
522+ "http://svc.ns:3000"
523+ ) ;
524+ }
525+
526+ #[ test]
527+ fn route_key_is_post_prefixed ( ) {
528+ assert_eq ! ( route_key( "/webhook/telegram" ) , "POST /webhook/telegram" ) ;
529+ assert_eq ! ( route_key( "/webhook/line" ) , "POST /webhook/line" ) ;
530+ }
531+
532+ #[ test]
533+ fn webhook_urls_join_endpoint_stage_and_path ( ) {
534+ let paths = vec ! [
535+ "/webhook/telegram" . to_string( ) ,
536+ "/webhook/line" . to_string( ) ,
537+ ] ;
538+ let urls = webhook_urls ( "https://abc123.execute-api.us-east-1.amazonaws.com" , & paths) ;
539+ assert_eq ! (
540+ urls,
541+ vec![
542+ "https://abc123.execute-api.us-east-1.amazonaws.com/prod/webhook/telegram" ,
543+ "https://abc123.execute-api.us-east-1.amazonaws.com/prod/webhook/line" ,
544+ ]
545+ ) ;
546+ }
547+
548+ #[ test]
549+ fn webhook_urls_trim_trailing_slash_on_endpoint ( ) {
550+ let paths = vec ! [ "/webhook/telegram" . to_string( ) ] ;
551+ let urls = webhook_urls ( "https://abc123.example.com/" , & paths) ;
552+ assert_eq ! ( urls, vec![ "https://abc123.example.com/prod/webhook/telegram" ] ) ;
553+ }
554+ }
0 commit comments