Repository: aws-cloudformation/cfn-lint

Issue:
Add a rule to avoid non-strings in template outputs
### Is this feature request related to a new rule or cfn-lint capabilities?

_No response_

### Describe the feature you'd like to request

Recently I had an issue deploying to cloudformation and I found that the issue was an exported number value.

Therefore I suggest you to add a lint rule to check output values for references to numbered values (just for references to resource attributes).

### Describe the solution you'd like

I assume the linter have access to the JSON schema definition and the solution should be a matter of reference solving and checking.

I think a check for the `GetAtt` function should be enough.

### Additional context

I was trying to deploy a MemoryDB cluster and a sibling stack was referencing the cluster port. I'm using the CDK.

### Is this something that you'd be interested in working on?

- [ ] 👋 I may be able to implement this feature request

### Would this feature include a breaking change?

- [ ] ⚠️ This feature might incur a breaking change


Discussion:
Agreed.  Looking into this.  I thought we had covered this but I need to clean some of the logic up.  Turns out if you do `Value: 1` that it works but if you do `Value: !Gett Resource.NonStringProperty` it will fail.  

Its bringing up that this issue extends beyond just Output Value and also exists in Sub, Joins, etc.





Patch:
diff --git a/scripts/update_snapshot_results.sh b/scripts/update_snapshot_results.sh
index 6e02e683b6..efb1b5d660 100755
--- a/scripts/update_snapshot_results.sh
+++ b/scripts/update_snapshot_results.sh
@@ -5,6 +5,7 @@ cfn-lint test/fixtures/templates/integration/dynamic-references.yaml -e -c I --f
 cfn-lint test/fixtures/templates/integration/resources-cloudformation-init.yaml -e -c I --format json > test/fixtures/results/integration/resources-cloudformation-init.json
 cfn-lint test/fixtures/templates/integration/ref-no-value.yaml -e -c I --format json > test/fixtures/results/integration/ref-no-value.json
 cfn-lint test/fixtures/templates/integration/availability-zones.yaml -e -c I --format json > test/fixtures/results/integration/availability-zones.json
+cfn-lint test/fixtures/templates/integration/getatt-types.yaml -e -c I --format json > test/fixtures/results/integration/getatt-types.json
 cfn-lint test/fixtures/templates/integration/aws-ec2-networkinterface.yaml -e -c I --format json > test/fixtures/results/integration/aws-ec2-networkinterface.json
 cfn-lint test/fixtures/templates/integration/aws-ec2-instance.yaml -e -c I --format json > test/fixtures/results/integration/aws-ec2-instance.json
 cfn-lint test/fixtures/templates/integration/aws-ec2-launchtemplate.yaml -e -c I --format json > test/fixtures/results/integration/aws-ec2-launchtemplate.json
diff --git a/src/cfnlint/data/schemas/other/step_functions/statemachine.json b/src/cfnlint/data/schemas/other/step_functions/statemachine.json
index 4af8362232..e05659700a 100644
--- a/src/cfnlint/data/schemas/other/step_functions/statemachine.json
+++ b/src/cfnlint/data/schemas/other/step_functions/statemachine.json
@@ -290,7 +290,7 @@
       "properties": {
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -303,7 +303,7 @@
        "ErrorEquals",
        "Next"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
@@ -418,7 +418,7 @@
        },
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -434,7 +434,7 @@
       "required": [
        "ErrorEquals"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
@@ -481,7 +481,7 @@
       "properties": {
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -494,7 +494,7 @@
        "ErrorEquals",
        "Next"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
@@ -540,7 +540,7 @@
        },
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -556,7 +556,7 @@
       "required": [
        "ErrorEquals"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
@@ -799,7 +799,7 @@
       "properties": {
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -812,7 +812,7 @@
        "ErrorEquals",
        "Next"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
@@ -880,7 +880,7 @@
        },
        "ErrorEquals": {
         "items": {
-         "types": "string"
+         "type": "string"
         },
         "type": "array"
        },
@@ -896,7 +896,7 @@
       "required": [
        "ErrorEquals"
       ],
-      "types": "object"
+      "type": "object"
      },
      "type": "array"
     },
diff --git a/src/cfnlint/jsonschema/_resolvers_cfn.py b/src/cfnlint/jsonschema/_resolvers_cfn.py
index 9f376ac112..8b2be3e2ed 100644
--- a/src/cfnlint/jsonschema/_resolvers_cfn.py
+++ b/src/cfnlint/jsonschema/_resolvers_cfn.py
@@ -248,8 +248,8 @@ def join(validator: Validator, instance: Any) -> ResolutionResult:
                 continue
             try:
                 for value in _join_expansion(values_v, values):
-                    yield delimiter.join(value), values_v, None
-            except ValueError:
+                    yield delimiter.join([str(v) for v in value]), values_v, None
+            except (ValueError, TypeError):
                 return
 
 
diff --git a/src/cfnlint/rules/functions/GetAtt.py b/src/cfnlint/rules/functions/GetAtt.py
index f33f3692f2..1780107d3a 100644
--- a/src/cfnlint/rules/functions/GetAtt.py
+++ b/src/cfnlint/rules/functions/GetAtt.py
@@ -129,9 +129,13 @@ def _resolve_getatt(
                         schema_types = ensure_list(getatt_schema.get("type"))
                         types = ensure_list(s.get("type"))
 
-                        if is_types_compatible(
-                            types, schema_types, validator.context.strict_types
-                        ):
+                        # GetAtt type checking is strict.
+                        # It must match in all cases
+                        if any(t in ["boolean", "integer", "boolean"] for t in types):
+                            # this should be switched to validate the value of the
+                            # property if it was available
+                            continue
+                        if is_types_compatible(types, schema_types, True):
                             continue
 
                         reprs = ", ".join(repr(type) for type in types)
diff --git a/src/cfnlint/rules/outputs/Value.py b/src/cfnlint/rules/outputs/Value.py
index 9d0bb6c41d..66f504f761 100644
--- a/src/cfnlint/rules/outputs/Value.py
+++ b/src/cfnlint/rules/outputs/Value.py
@@ -50,10 +50,7 @@ def validate(self, validator: Validator, _: Any, instance: Any, schema: Any):
         for err in validator.descend(
             value,
             schema={
-                "type": ["array", "string"],
-                "items": {
-                    "type": "string",
-                },
+                "type": ["string"],
             },
             path=key,
             property_path=key,


Test patch:
diff --git a/test/fixtures/results/integration/getatt-types.json b/test/fixtures/results/integration/getatt-types.json
new file mode 100644
index 0000000000..bcf56c7db9
--- /dev/null
+++ b/test/fixtures/results/integration/getatt-types.json
@@ -0,0 +1,243 @@
+[
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "ee655930-182c-2be9-568e-6377b79b5798",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 12,
+                "LineNumber": 18
+            },
+            "Path": [
+                "Resources",
+                "SsmParameter",
+                "Properties",
+                "Value",
+                "Fn::GetAtt"
+            ],
+            "Start": {
+                "ColumnNumber": 7,
+                "LineNumber": 18
+            }
+        },
+        "Message": "{'Fn::GetAtt': ['CapacityReservation', 'InstanceCount']} is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Validates that GetAtt parameters are to valid resources and properties of those resources",
+            "Id": "E1010",
+            "ShortDescription": "GetAtt validation of parameters",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "76c89a87-5cd1-4dcd-1050-b5dc891de1fe",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 69,
+                "LineNumber": 29
+            },
+            "Path": [
+                "Outputs",
+                "SubWithGetAtt",
+                "Value",
+                "Fn::Sub",
+                1,
+                "InstanceCount",
+                "Fn::GetAtt"
+            ],
+            "Start": {
+                "ColumnNumber": 57,
+                "LineNumber": 29
+            }
+        },
+        "Message": "{'Fn::GetAtt': ['CapacityReservation', 'InstanceCount']} is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Make sure that output values have a type of string",
+            "Id": "E6101",
+            "ShortDescription": "Validate that outputs values are a string",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "efe80ed4-7370-e389-6d0f-9e5234a86db9",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 10,
+                "LineNumber": 31
+            },
+            "Path": [
+                "Outputs",
+                "Ipv4NetmaskLength",
+                "Value",
+                "Fn::GetAtt"
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 31
+            }
+        },
+        "Message": "{'Fn::GetAtt': ['CapacityReservation', 'InstanceCount']} is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Make sure that output values have a type of string",
+            "Id": "E6101",
+            "ShortDescription": "Validate that outputs values are a string",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "75ebbb04-db20-4d32-4546-8fe0b984ef73",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 10,
+                "LineNumber": 33
+            },
+            "Path": [
+                "Outputs",
+                "String",
+                "Value",
+                "Fn::Sub"
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 33
+            }
+        },
+        "Message": "'CapacityReservation.InstanceCount' is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Make sure that output values have a type of string",
+            "Id": "E6101",
+            "ShortDescription": "Validate that outputs values are a string",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "5dd605a3-2dee-84bd-905b-ddc1a10cc24f",
+        "Level": "Informational",
+        "Location": {
+            "End": {
+                "ColumnNumber": 22,
+                "LineNumber": 35
+            },
+            "Path": [
+                "Outputs",
+                "Join",
+                "Value",
+                "Fn::Join",
+                0
+            ],
+            "Start": {
+                "ColumnNumber": 20,
+                "LineNumber": 35
+            }
+        },
+        "Message": "Prefer using Fn::Sub over Fn::Join with an empty delimiter",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Prefer a sub instead of Join when using a join delimiter that is empty",
+            "Id": "I1022",
+            "ShortDescription": "Use Sub instead of Join",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "518fa518-bd82-b365-8ec2-c48ae8581f3e",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 10,
+                "LineNumber": 37
+            },
+            "Path": [
+                "Outputs",
+                "JoinWithGetAtt",
+                "Value",
+                "Fn::Join",
+                1,
+                0,
+                "Fn::GetAtt"
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 37
+            }
+        },
+        "Message": "{'Fn::GetAtt': ['CapacityReservation', 'InstanceCount']} is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Make sure that output values have a type of string",
+            "Id": "E6101",
+            "ShortDescription": "Validate that outputs values are a string",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "59af0561-c0a4-cef0-bca5-bcd57975d7b3",
+        "Level": "Informational",
+        "Location": {
+            "End": {
+                "ColumnNumber": 22,
+                "LineNumber": 37
+            },
+            "Path": [
+                "Outputs",
+                "JoinWithGetAtt",
+                "Value",
+                "Fn::Join",
+                0
+            ],
+            "Start": {
+                "ColumnNumber": 20,
+                "LineNumber": 37
+            }
+        },
+        "Message": "Prefer using Fn::Sub over Fn::Join with an empty delimiter",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Prefer a sub instead of Join when using a join delimiter that is empty",
+            "Id": "I1022",
+            "ShortDescription": "Use Sub instead of Join",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/integration/getatt-types.yaml",
+        "Id": "3b709659-f299-68e5-3bac-f9835c487de3",
+        "Level": "Error",
+        "Location": {
+            "End": {
+                "ColumnNumber": 10,
+                "LineNumber": 41
+            },
+            "Path": [
+                "Outputs",
+                "BooleanGetAtt",
+                "Value",
+                "Fn::GetAtt"
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 41
+            }
+        },
+        "Message": "{'Fn::GetAtt': ['CapacityReservation', 'EphemeralStorage']} is not of type 'string'",
+        "ParentId": null,
+        "Rule": {
+            "Description": "Make sure that output values have a type of string",
+            "Id": "E6101",
+            "ShortDescription": "Validate that outputs values are a string",
+            "Source": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html"
+        }
+    }
+]
diff --git a/test/fixtures/templates/integration/getatt-types.yaml b/test/fixtures/templates/integration/getatt-types.yaml
new file mode 100644
index 0000000000..3c15eb5615
--- /dev/null
+++ b/test/fixtures/templates/integration/getatt-types.yaml
@@ -0,0 +1,43 @@
+Parameters:
+  ANumber:
+    Type: Number
+    Default: 1
+Transform: 'AWS::LanguageExtensions'
+Resources:
+  CapacityReservation:
+    Type: AWS::EC2::CapacityReservation
+    Properties:
+      AvailabilityZone: !Select [0, !GetAZs ""]
+      InstanceCount: 1
+      InstanceType: t2.micro
+      InstancePlatform: Linux/UNIX
+  SsmParameter:
+    Type: AWS::SSM::Parameter
+    Properties:
+      Type: String
+      Value: !GetAtt CapacityReservation.InstanceCount      # #/Value: expected type: String, found: Integer
+Outputs:
+  Value:
+    Value: 1 # OK
+  Sub:
+    Value: !Sub ["${InstanceCount}", {"InstanceCount": 1}] # OK
+  RefWithParameter:
+    Value: !Ref ANumber # OK
+  SubWithParameter:
+    Value: !Sub "${ANumber}"  # OK
+  SubWithGetAtt:
+    Value: !Sub ["${InstanceCount}", {"InstanceCount": {"Fn::GetAtt": ["CapacityReservation", "InstanceCount"]}}] # Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string
+  Ipv4NetmaskLength:
+    Value: !GetAtt CapacityReservation.InstanceCount # Template format error: Every Value member must be a string.
+  String:
+    Value: !Sub "${CapacityReservation.InstanceCount}"  # Template error: variable CapacityReservation.InstanceCount in Fn::Sub expression does not resolve to a string
+  Join:
+    Value: !Join [ "", [1]]  # OK
+  JoinWithGetAtt:
+    Value: !Join [ "", [!GetAtt CapacityReservation.InstanceCount]]  # Template error: every Fn::Join object requires two parameters, (1) a string delimiter and (2) a list of strings to be joined or a function that returns a list of strings (such as Fn::GetAZs) to be joined.
+  Boolean:
+    Value: true # OK
+  BooleanGetAtt:
+    Value: !GetAtt CapacityReservation.EphemeralStorage  # Template format error: Every Value member must be a string.
+  Length:
+    Value: {Fn::Length: [1, 2, 3]} # OK
diff --git a/test/integration/test_integration_templates.py b/test/integration/test_integration_templates.py
index 99131501c7..761ff40f57 100644
--- a/test/integration/test_integration_templates.py
+++ b/test/integration/test_integration_templates.py
@@ -47,6 +47,11 @@ class TestQuickStartTemplates(BaseCliTestCase):
             ),
             "exit_code": 2,
         },
+        {
+            "filename": ("test/fixtures/templates/integration/getatt-types.yaml"),
+            "results_filename": ("test/fixtures/results/integration/getatt-types.json"),
+            "exit_code": 10,
+        },
         {
             "filename": (
                 "test/fixtures/templates/integration/aws-ec2-networkinterface.yaml"
@@ -87,11 +92,6 @@ def test_templates(self):
             ConfigMixIn(
                 [],
                 include_checks=["I"],
-                configure_rules={
-                    "E3012": {
-                        "strict": True,
-                    }
-                },
                 include_experimental=True,
             )
         )
diff --git a/test/unit/rules/functions/test_getatt.py b/test/unit/rules/functions/test_getatt.py
index e1c54f0dfa..aedc3af866 100644
--- a/test/unit/rules/functions/test_getatt.py
+++ b/test/unit/rules/functions/test_getatt.py
@@ -7,12 +7,9 @@
 
 import pytest
 
-from cfnlint.context import create_context_for_template
-from cfnlint.context.context import Transforms
-from cfnlint.jsonschema import CfnTemplateValidator, ValidationError
+from cfnlint.jsonschema import ValidationError
 from cfnlint.rules import CfnLintKeyword
 from cfnlint.rules.functions.GetAtt import GetAtt
-from cfnlint.template import Template
 
 
 @pytest.fixture(scope="module")
@@ -21,27 +18,19 @@ def rule():
     yield rule
 
 
-@pytest.fixture(scope="module")
-def cfn():
-    return Template(
-        "",
-        {
-            "Resources": {
-                "MyBucket": {"Type": "AWS::S3::Bucket"},
-                "MyCodePipeline": {"Type": "AWS::CodePipeline::Pipeline"},
-            },
-            "Parameters": {
-                "MyResourceParameter": {"Type": "String", "Default": "MyBucket"},
-                "MyAttributeParameter": {"Type": "String", "AllowedValues": ["Arn"]},
-            },
-        },
-        regions=["us-east-1"],
-    )
-
+_template = {
+    "Resources": {
+        "MyBucket": {"Type": "AWS::S3::Bucket"},
+        "MyCodePipeline": {"Type": "AWS::CodePipeline::Pipeline"},
+    },
+    "Parameters": {
+        "MyResourceParameter": {"Type": "String", "Default": "MyBucket"},
+        "MyAttributeParameter": {"Type": "String", "AllowedValues": ["Arn"]},
+    },
+}
 
-@pytest.fixture(scope="module")
-def context(cfn):
-    return create_context_for_template(cfn)
+_template_with_transform = _template.copy()
+_template_with_transform["Transform"] = "AWS::LanguageExtensions"
 
 
 class _Pass(CfnLintKeyword):
@@ -66,13 +55,13 @@ def validate(self, validator, s, instance, schema):
 
 
 @pytest.mark.parametrize(
-    "name,instance,schema,context_evolve,child_rules,expected",
+    "name,instance,schema,template,child_rules,expected",
     [
         (
             "Valid GetAtt with a good attribute",
             {"Fn::GetAtt": ["MyBucket", "Arn"]},
             {"type": "string"},
-            {},
+            _template,
             {},
             [],
         ),
@@ -80,7 +69,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid GetAtt with bad attribute",
             {"Fn::GetAtt": ["MyBucket", "foo"]},
             {"type": "string"},
-            {},
+            _template,
             {},
             [
                 ValidationError(
@@ -99,7 +88,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid GetAtt with bad resource name",
             {"Fn::GetAtt": ["Foo", "bar"]},
             {"type": "string"},
-            {},
+            _template,
             {},
             [
                 ValidationError(
@@ -114,7 +103,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid GetAtt with a bad type",
             {"Fn::GetAtt": {"foo": "bar"}},
             {"type": "string"},
-            {},
+            _template,
             {},
             [
                 ValidationError(
@@ -129,7 +118,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid GetAtt with a bad response type",
             {"Fn::GetAtt": "MyBucket.Arn"},
             {"type": "array"},
-            {},
+            _template,
             {},
             [
                 ValidationError(
@@ -144,7 +133,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid GetAtt with a bad response type and multiple types",
             {"Fn::GetAtt": "MyBucket.Arn"},
             {"type": ["array", "object"]},
-            {},
+            _template,
             {},
             [
                 ValidationError(
@@ -159,37 +148,35 @@ def validate(self, validator, s, instance, schema):
             "Valid GetAtt with integer to string",
             {"Fn::GetAtt": "MyCodePipeline.Version"},
             {"type": ["integer"]},
-            {
-                "strict_types": False,
-            },
+            _template,
             {},
             [],
         ),
-        (
-            "Invalid GetAtt with integer to string",
-            {"Fn::GetAtt": "MyCodePipeline.Version"},
-            {"type": ["integer"]},
-            {
-                "strict_types": True,
-            },
-            {},
-            [
-                ValidationError(
-                    (
-                        "{'Fn::GetAtt': 'MyCodePipeline.Version'} "
-                        "is not of type 'integer'"
-                    ),
-                    path=deque(["Fn::GetAtt"]),
-                    schema_path=deque(["type"]),
-                    validator="fn_getatt",
-                )
-            ],
-        ),
+        # (
+        #    "Invalid GetAtt with integer to string",
+        #    {"Fn::GetAtt": "MyCodePipeline.Version"},
+        #    {"type": ["integer"]},
+        #    {
+        #        "strict_types": True,
+        #    },
+        #    {},
+        #    [
+        #        ValidationError(
+        #            (
+        #                "{'Fn::GetAtt': 'MyCodePipeline.Version'} "
+        #                "is not of type 'integer'"
+        #            ),
+        #            path=deque(["Fn::GetAtt"]),
+        #            schema_path=deque(["type"]),
+        #            validator="fn_getatt",
+        #        )
+        #    ],
+        # ),
         (
             "Valid GetAtt with one good response type",
             {"Fn::GetAtt": "MyBucket.Arn"},
             {"type": ["array", "string"]},
-            {},
+            _template,
             {},
             [],
         ),
@@ -197,7 +184,7 @@ def validate(self, validator, s, instance, schema):
             "Valid Ref in GetAtt for resource",
             {"Fn::GetAtt": [{"Ref": "MyResourceParameter"}, "Arn"]},
             {"type": "string"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            _template_with_transform,
             {},
             [],
         ),
@@ -205,7 +192,7 @@ def validate(self, validator, s, instance, schema):
             "Valid Ref in GetAtt for attribute",
             {"Fn::GetAtt": ["MyBucket", {"Ref": "MyAttributeParameter"}]},
             {"type": "string"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            _template_with_transform,
             {},
             [],
         ),
@@ -213,7 +200,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid Ref in GetAtt for attribute",
             {"Fn::GetAtt": ["MyBucket", {"Ref": "MyResourceParameter"}]},
             {"type": "string"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            _template_with_transform,
             {},
             [
                 ValidationError(
@@ -232,7 +219,7 @@ def validate(self, validator, s, instance, schema):
             "Invalid Ref in GetAtt for attribute",
             {"Fn::GetAtt": [{"Ref": "MyAttributeParameter"}, "Arn"]},
             {"type": "string"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            _template_with_transform,
             {},
             [
                 ValidationError(
@@ -250,7 +237,7 @@ def validate(self, validator, s, instance, schema):
             "Valid GetAtt with child rules",
             {"Fn::GetAtt": ["MyBucket", "Arn"]},
             {"type": "string"},
-            {},
+            _template,
             {
                 "AAAAA": _Pass(),
                 "BBBBB": _Fail(),
@@ -259,13 +246,12 @@ def validate(self, validator, s, instance, schema):
             [ValidationError("Fail")],
         ),
     ],
+    indirect=["template"],
 )
 def test_validate(
-    name, instance, schema, context_evolve, child_rules, expected, rule, context, cfn
+    name, instance, schema, template, child_rules, expected, validator, rule
 ):
-    context = context.evolve(**context_evolve)
     rule.child_rules = child_rules
-    validator = CfnTemplateValidator({}, context=context, cfn=cfn)
     errs = list(rule.fn_getatt(validator, schema, instance, {}))
 
     assert errs == expected, f"Test {name!r} got {errs!r}"
diff --git a/test/unit/rules/functions/test_length.py b/test/unit/rules/functions/test_length.py
index a6b88a4bb0..6ddc0cce85 100644
--- a/test/unit/rules/functions/test_length.py
+++ b/test/unit/rules/functions/test_length.py
@@ -7,35 +7,18 @@
 
 import pytest
 
-from cfnlint.context import create_context_for_template
-from cfnlint.context.context import Transforms
-from cfnlint.jsonschema import CfnTemplateValidator, ValidationError
+from cfnlint.jsonschema import ValidationError
 from cfnlint.rules.functions.Length import Length
-from cfnlint.template import Template
 
 
-@pytest.fixture(scope="module")
+@pytest.fixture
 def rule():
     rule = Length()
     yield rule
 
 
-@pytest.fixture(scope="module")
-def cfn():
-    return Template(
-        "",
-        {},
-        regions=["us-east-1"],
-    )
-
-
-@pytest.fixture(scope="module")
-def context(cfn):
-    return create_context_for_template(cfn)
-
-
 @pytest.mark.parametrize(
-    "name,instance,schema,context_evolve,expected",
+    "name,instance,schema,template,expected",
     [
         (
             "Fn::Length is not supported",
@@ -59,14 +42,14 @@ def context(cfn):
             "Fn::Length valid structure",
             {"Fn::Length": []},
             {"type": "integer"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [],
         ),
         (
             "Fn::Length invalid type",
             {"Fn::Length": "foo"},
             {"type": "integer"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [
                 ValidationError(
                     "'foo' is not of type 'array'",
@@ -81,7 +64,7 @@ def context(cfn):
             "Fn::Length invalid output type",
             {"Fn::Length": ["foo"]},
             {"type": "array"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [
                 ValidationError(
                     "{'Fn::Length': ['foo']} is not of type 'array'",
@@ -96,34 +79,33 @@ def context(cfn):
             "Fn::Length using valid function",
             {"Fn::Length": {"Fn::GetAZs": ""}},
             {"type": "integer"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [],
         ),
         (
             "Fn::Length using valid functions in array",
             {"Fn::Length": [{"Ref": "MyResource"}]},
             {"type": "integer"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [],
         ),
         (
             "Fn::Length is not supported",
             {"Fn::Length": []},
             {"type": "integer"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [],
         ),
         (
             "Fn::Length output while a number can be a string",
             {"Fn::Length": []},
             {"type": "string"},
-            {"transforms": Transforms(["AWS::LanguageExtensions"])},
+            {"Transform": ["AWS::LanguageExtensions"]},
             [],
         ),
     ],
+    indirect=["template"],
 )
-def test_validate(name, instance, schema, context_evolve, expected, rule, context, cfn):
-    context = context.evolve(**context_evolve)
-    validator = CfnTemplateValidator(context=context, cfn=cfn)
+def test_validate(name, instance, schema, expected, validator, rule):
     errs = list(rule.fn_length(validator, schema, instance, {}))
     assert errs == expected, f"Test {name!r} got {errs!r}"
diff --git a/test/unit/rules/outputs/test_value.py b/test/unit/rules/outputs/test_value.py
index ec1b481d45..6fdb3b8d5f 100644
--- a/test/unit/rules/outputs/test_value.py
+++ b/test/unit/rules/outputs/test_value.py
@@ -103,7 +103,7 @@ def validator(cfn, context):
             },
             [
                 ValidationError(
-                    "1.0 is not of type 'array', 'string'",
+                    "1.0 is not of type 'string'",
                     validator="type",
                     schema_path=deque(["type"]),
                     path=deque(["Value"]),
@@ -117,7 +117,7 @@ def validator(cfn, context):
             },
             [
                 ValidationError(
-                    "1 is not of type 'array', 'string'",
+                    "1 is not of type 'string'",
                     validator="type",
                     schema_path=deque(["type"]),
                     path=deque(["Value"]),
@@ -129,7 +129,7 @@ def validator(cfn, context):
             {"Value": True},
             [
                 ValidationError(
-                    "True is not of type 'array', 'string'",
+                    "True is not of type 'string'",
                     validator="type",
                     schema_path=deque(["type"]),
                     path=deque(["Value"]),
@@ -141,10 +141,10 @@ def validator(cfn, context):
             {"Value": [{}]},
             [
                 ValidationError(
-                    "{} is not of type 'string'",
+                    "[{}] is not of type 'string'",
                     validator="type",
-                    schema_path=deque(["items", "type"]),
-                    path=deque(["Value", 0]),
+                    schema_path=deque(["type"]),
+                    path=deque(["Value"]),
                     rule=Value(),
                 )
             ],
@@ -153,7 +153,7 @@ def validator(cfn, context):
             {"Value": {"foo": "bar"}},
             [
                 ValidationError(
-                    "{'foo': 'bar'} is not of type 'array', 'string'",
+                    "{'foo': 'bar'} is not of type 'string'",
                     validator="type",
                     schema_path=deque(["type"]),
                     path=deque(["Value"]),