@@ -521,7 +521,332 @@ describe("StaticHosting", () => {
521521 } ,
522522 } ) ;
523523 } ) ;
524+ } ) ;
525+
526+ describe ( "CORS Configuration" , ( ) => {
527+ it ( "should not create CORS policy when corsConfig is not provided" , ( ) => {
528+ const { stack } = createTestStack ( ) ;
529+ const hosting = new StaticHosting ( stack , "TestConstruct" , defaultProps ) ;
530+
531+ expect ( hosting . corsResponseHeadersPolicy ) . toBeUndefined ( ) ;
532+ } ) ;
533+
534+ it ( "should create CORS policy with defaults when corsConfig is provided" , ( ) => {
535+ const { stack } = createTestStack ( ) ;
536+ const hosting = new StaticHosting ( stack , "TestConstruct" , {
537+ ...defaultProps ,
538+ corsConfig : {
539+ accessControlAllowOrigins : [
540+ "https://example.com" ,
541+ "https://app.example.com" ,
542+ ] ,
543+ } ,
544+ } ) ;
545+
546+ expect ( hosting . corsResponseHeadersPolicy ) . toBeDefined ( ) ;
547+
548+ const template = Template . fromStack ( stack ) ;
549+ template . hasResourceProperties ( "AWS::CloudFront::ResponseHeadersPolicy" , {
550+ ResponseHeadersPolicyConfig : {
551+ CorsConfig : {
552+ AccessControlAllowCredentials : false ,
553+ AccessControlAllowHeaders : {
554+ Items : [ "*" ] ,
555+ } ,
556+ AccessControlAllowMethods : {
557+ Items : [ "GET" , "HEAD" , "OPTIONS" ] ,
558+ } ,
559+ AccessControlAllowOrigins : {
560+ Items : [ "https://example.com" , "https://app.example.com" ] ,
561+ } ,
562+ OriginOverride : true ,
563+ } ,
564+ } ,
565+ } ) ;
566+ } ) ;
567+
568+ it ( "should create CORS policy with custom settings when provided" , ( ) => {
569+ const { stack } = createTestStack ( ) ;
570+ new StaticHosting ( stack , "TestConstruct" , {
571+ ...defaultProps ,
572+ corsConfig : {
573+ accessControlAllowOrigins : [ "https://example.com" ] ,
574+ accessControlAllowCredentials : true ,
575+ accessControlAllowHeaders : [ "Content-Type" , "Authorization" ] ,
576+ accessControlAllowMethods : [ "GET" , "HEAD" , "OPTIONS" , "POST" ] ,
577+ originOverride : false ,
578+ } ,
579+ } ) ;
580+
581+ const template = Template . fromStack ( stack ) ;
582+ template . hasResourceProperties ( "AWS::CloudFront::ResponseHeadersPolicy" , {
583+ ResponseHeadersPolicyConfig : {
584+ CorsConfig : {
585+ AccessControlAllowCredentials : true ,
586+ AccessControlAllowHeaders : {
587+ Items : [ "Content-Type" , "Authorization" ] ,
588+ } ,
589+ AccessControlAllowMethods : {
590+ Items : [ "GET" , "HEAD" , "OPTIONS" , "POST" ] ,
591+ } ,
592+ AccessControlAllowOrigins : {
593+ Items : [ "https://example.com" ] ,
594+ } ,
595+ OriginOverride : false ,
596+ } ,
597+ } ,
598+ } ) ;
599+ } ) ;
600+
601+ it ( "should apply CORS policy to static file behaviors" , ( ) => {
602+ const { stack } = createTestStack ( ) ;
603+ new StaticHosting ( stack , "TestConstruct" , {
604+ ...defaultProps ,
605+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
606+ } ) ;
607+
608+ const template = Template . fromStack ( stack ) ;
609+ const distribution = template . findResources (
610+ "AWS::CloudFront::Distribution"
611+ ) ;
612+ const distConfig =
613+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
614+
615+ // Check that static file behaviors have a response headers policy
616+ const jsBehavior = distConfig . CacheBehaviors . find (
617+ ( b : { PathPattern : string } ) => b . PathPattern === "*.js"
618+ ) ;
619+ expect ( jsBehavior ) . toBeDefined ( ) ;
620+ expect ( jsBehavior . ResponseHeadersPolicyId ) . toBeDefined ( ) ;
621+
622+ const cssBehavior = distConfig . CacheBehaviors . find (
623+ ( b : { PathPattern : string } ) => b . PathPattern === "*.css"
624+ ) ;
625+ expect ( cssBehavior ) . toBeDefined ( ) ;
626+ expect ( cssBehavior . ResponseHeadersPolicyId ) . toBeDefined ( ) ;
627+ } ) ;
628+
629+ it ( "should not apply CORS policy to static files when accessControlAllowOrigins is empty array" , ( ) => {
630+ const { stack } = createTestStack ( ) ;
631+ const hosting = new StaticHosting ( stack , "TestConstruct" , {
632+ ...defaultProps ,
633+ corsConfig : { accessControlAllowOrigins : [ ] } ,
634+ } ) ;
635+
636+ expect ( hosting . corsResponseHeadersPolicy ) . toBeUndefined ( ) ;
637+
638+ const template = Template . fromStack ( stack ) ;
639+ const distribution = template . findResources (
640+ "AWS::CloudFront::Distribution"
641+ ) ;
642+ const distConfig =
643+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
644+
645+ // Check that static file behaviors do not have a response headers policy
646+ const jsBehavior = distConfig . CacheBehaviors . find (
647+ ( b : { PathPattern : string } ) => b . PathPattern === "*.js"
648+ ) ;
649+ expect ( jsBehavior ) . toBeDefined ( ) ;
650+ expect ( jsBehavior . ResponseHeadersPolicyId ) . toBeUndefined ( ) ;
651+ } ) ;
652+
653+ it ( "should expose corsResponseHeadersPolicy for downstream use" , ( ) => {
654+ const { stack } = createTestStack ( ) ;
655+ const hosting = new StaticHosting ( stack , "TestConstruct" , {
656+ ...defaultProps ,
657+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
658+ } ) ;
659+
660+ // The policy should be accessible for downstream projects to use
661+ // in their own custom behaviors
662+ expect ( hosting . corsResponseHeadersPolicy ) . toBeDefined ( ) ;
663+ expect ( typeof hosting . corsResponseHeadersPolicy ) . toBe ( "object" ) ;
664+ } ) ;
665+
666+ it ( "should apply CORS policy to remapPaths behaviors" , ( ) => {
667+ const { stack } = createTestStack ( ) ;
668+ new StaticHosting ( stack , "TestConstruct" , {
669+ ...defaultProps ,
670+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
671+ remapPaths : [ { from : "/test-path" , to : "/remapped-path" } ] ,
672+ } ) ;
673+
674+ const template = Template . fromStack ( stack ) ;
675+ const distribution = template . findResources (
676+ "AWS::CloudFront::Distribution"
677+ ) ;
678+ const distConfig =
679+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
680+
681+ const remapBehavior = distConfig . CacheBehaviors . find (
682+ ( b : { PathPattern : string } ) => b . PathPattern === "/test-path"
683+ ) ;
684+ expect ( remapBehavior ) . toBeDefined ( ) ;
685+ expect ( remapBehavior . ResponseHeadersPolicyId ) . toBeDefined ( ) ;
686+ } ) ;
687+
688+ it ( "should apply CORS policy to remapBackendPaths behaviors" , ( ) => {
689+ const { stack } = createTestStack ( ) ;
690+ new StaticHosting ( stack , "TestConstruct" , {
691+ ...defaultProps ,
692+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
693+ backendHost : "backend.example.com" ,
694+ remapBackendPaths : [ { from : "/api/*" , to : "/api/*" } ] ,
695+ } ) ;
696+
697+ const template = Template . fromStack ( stack ) ;
698+ const distribution = template . findResources (
699+ "AWS::CloudFront::Distribution"
700+ ) ;
701+ const distConfig =
702+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
703+
704+ const backendBehavior = distConfig . CacheBehaviors . find (
705+ ( b : { PathPattern : string } ) => b . PathPattern === "/api/*"
706+ ) ;
707+ expect ( backendBehavior ) . toBeDefined ( ) ;
708+ expect ( backendBehavior . ResponseHeadersPolicyId ) . toBeDefined ( ) ;
709+ } ) ;
710+
711+ it ( "should apply CORS to default behavior when indexable is true" , ( ) => {
712+ const { stack } = createTestStack ( ) ;
713+ new StaticHosting ( stack , "TestConstruct" , {
714+ ...defaultProps ,
715+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
716+ indexable : true ,
717+ } ) ;
718+
719+ const template = Template . fromStack ( stack ) ;
720+ const distribution = template . findResources (
721+ "AWS::CloudFront::Distribution"
722+ ) ;
723+ const distConfig =
724+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
725+
726+ // Default behavior should have response headers policy (CORS)
727+ expect (
728+ distConfig . DefaultCacheBehavior . ResponseHeadersPolicyId
729+ ) . toBeDefined ( ) ;
730+ } ) ;
731+ } ) ;
732+
733+ describe ( "Indexable Configuration" , ( ) => {
734+ it ( "should not create NoIndexNoFollow policy when indexable is true (default)" , ( ) => {
735+ const { stack } = createTestStack ( ) ;
736+ new StaticHosting ( stack , "TestConstruct" , defaultProps ) ;
737+
738+ const template = Template . fromStack ( stack ) ;
739+ const policies = template . findResources (
740+ "AWS::CloudFront::ResponseHeadersPolicy"
741+ ) ;
742+
743+ // Should not have a NoIndexNoFollow policy
744+ const hasNoIndexPolicy = Object . values ( policies ) . some (
745+ ( policy : Record < string , unknown > ) => {
746+ const config = (
747+ policy . Properties as {
748+ ResponseHeadersPolicyConfig ?: {
749+ CustomHeadersConfig ?: {
750+ Items ?: Array < { Header : string ; Value : string } > ;
751+ } ;
752+ } ;
753+ }
754+ ) ?. ResponseHeadersPolicyConfig ?. CustomHeadersConfig ?. Items ;
755+ return config ?. some (
756+ item =>
757+ item . Header === "x-robots-tag" &&
758+ item . Value === "noindex,nofollow"
759+ ) ;
760+ }
761+ ) ;
762+ expect ( hasNoIndexPolicy ) . toBe ( false ) ;
763+ } ) ;
764+
765+ it ( "should create NoIndexNoFollow policy when indexable is false" , ( ) => {
766+ const { stack } = createTestStack ( ) ;
767+ new StaticHosting ( stack , "TestConstruct" , {
768+ ...defaultProps ,
769+ indexable : false ,
770+ } ) ;
771+
772+ const template = Template . fromStack ( stack ) ;
773+ template . hasResourceProperties ( "AWS::CloudFront::ResponseHeadersPolicy" , {
774+ ResponseHeadersPolicyConfig : {
775+ CustomHeadersConfig : {
776+ Items : [
777+ {
778+ Header : "x-robots-tag" ,
779+ Value : "noindex,nofollow" ,
780+ Override : true ,
781+ } ,
782+ ] ,
783+ } ,
784+ } ,
785+ } ) ;
786+ } ) ;
787+
788+ it ( "should combine NoIndexNoFollow with CORS when both indexable is false and corsConfig is set" , ( ) => {
789+ const { stack } = createTestStack ( ) ;
790+ new StaticHosting ( stack , "TestConstruct" , {
791+ ...defaultProps ,
792+ indexable : false ,
793+ corsConfig : { accessControlAllowOrigins : [ "https://example.com" ] } ,
794+ } ) ;
795+
796+ const template = Template . fromStack ( stack ) ;
797+ // Should have a policy with both noindex and CORS
798+ template . hasResourceProperties ( "AWS::CloudFront::ResponseHeadersPolicy" , {
799+ ResponseHeadersPolicyConfig : {
800+ CustomHeadersConfig : {
801+ Items : [
802+ {
803+ Header : "x-robots-tag" ,
804+ Value : "noindex,nofollow" ,
805+ Override : true ,
806+ } ,
807+ ] ,
808+ } ,
809+ CorsConfig : {
810+ AccessControlAllowCredentials : false ,
811+ AccessControlAllowHeaders : {
812+ Items : [ "*" ] ,
813+ } ,
814+ AccessControlAllowMethods : {
815+ Items : [ "GET" , "HEAD" , "OPTIONS" ] ,
816+ } ,
817+ AccessControlAllowOrigins : {
818+ Items : [ "https://example.com" ] ,
819+ } ,
820+ OriginOverride : true ,
821+ } ,
822+ } ,
823+ } ) ;
824+ } ) ;
825+
826+ it ( "should apply NoIndexNoFollow policy to default behavior" , ( ) => {
827+ const { stack } = createTestStack ( ) ;
828+ new StaticHosting ( stack , "TestConstruct" , {
829+ ...defaultProps ,
830+ indexable : false ,
831+ } ) ;
832+
833+ const template = Template . fromStack ( stack ) ;
834+ const distribution = template . findResources (
835+ "AWS::CloudFront::Distribution"
836+ ) ;
837+ const distConfig =
838+ Object . values ( distribution ) [ 0 ] . Properties . DistributionConfig ;
839+
840+ // Default behavior should have response headers policy
841+ expect (
842+ distConfig . DefaultCacheBehavior . ResponseHeadersPolicyId
843+ ) . toBeDefined ( ) ;
844+ } ) ;
845+ } ) ;
524846
847+ // NOTE: This test creates EdgeFunctions which can cause cross-app reference issues
848+ // in subsequent tests. Keep this test section last.
849+ describe ( "CSP Path Behaviors" , ( ) => {
525850 it ( "should create CSP path behaviors" , ( ) => {
526851 const { stack } = createTestStack ( ) ;
527852 new StaticHosting ( stack , "TestConstruct" , {
0 commit comments