@@ -174,6 +174,18 @@ def test_successful_parsing(self, input, expected):
174174 ("file://nested.yaml" ,),
175175 {"A" : "yaml" , "B" : "yaml" , "Toml" : "toml" , "Yaml" : "yaml" },
176176 ),
177+ (
178+ ("file://nested.toml" ,),
179+ {"A" : "toml" , "B" : "toml" , "Toml" : "toml" , "Env" : "production" },
180+ ),
181+ (
182+ ("file://nested-list.toml" ,),
183+ {"A" : "yaml" , "B" : "yaml" , "Toml" : "toml" , "Yaml" : "yaml" , "Env" : "prod" },
184+ ),
185+ (
186+ ("file://nested-list.yaml" ,),
187+ {"A" : "yaml" , "B" : "yaml" , "Toml" : "toml" , "Yaml" : "yaml" , "Env" : "prod" },
188+ ),
177189 ]
178190 )
179191 def test_merge_file_parsing (self , inputs , expected ):
@@ -182,6 +194,9 @@ def test_merge_file_parsing(self, inputs, expected):
182194 "params.yaml" : "A: yaml\n B: yaml\n Yaml: yaml" ,
183195 "list.yaml" : "- - - - - - A: a\n - List:\n - 1\n - 2\n - 3\n " ,
184196 "nested.yaml" : "- file://params.toml\n - file://params.yaml" ,
197+ "nested.toml" : "'$include' = 'file://params.toml'\n Env = 'production'" ,
198+ "nested-list.toml" : "'$include' = ['file://params.toml', 'file://params.yaml']\n Env = 'prod'" ,
199+ "nested-list.yaml" : "$include:\n - file://params.toml\n - file://params.yaml\n Env: prod" ,
185200 }
186201
187202 def mock_read_text (file_path ):
@@ -196,6 +211,27 @@ def mock_is_file(file_path):
196211 print (result )
197212 self .assertEqual (result , expected , msg = "Failed with Input = " + str (inputs ))
198213
214+ def test_include_infinite_recursion_protection (self ):
215+ mock_files = {
216+ "A.yaml" : "$include: file://B.yaml" ,
217+ "B.yaml" : "$include: file://C.yaml" ,
218+ "C.yaml" : "$include: file://A.yaml" ,
219+ }
220+
221+ def mock_read_text (file_path ):
222+ file_name = file_path .name
223+ return mock_files .get (file_name , "" )
224+
225+ def mock_is_file (file_path ):
226+ return file_path .name in mock_files
227+
228+ with self .assertRaises (BadParameter ) as exception , patch ("pathlib.Path.is_file" , new = mock_is_file ), patch (
229+ "pathlib.Path.read_text" , new = mock_read_text
230+ ):
231+ self .param_type .convert ("file://A.yaml" , None , MagicMock ())
232+
233+ self .assertIn ("Infinite recursion detected in file references" , str (exception .exception ))
234+
199235 def test_infinite_recursion_protection (self ):
200236 mock_files = {
201237 "A.yaml" : "- file://B.yaml" ,
@@ -213,10 +249,67 @@ def mock_is_file(file_path):
213249 with self .assertRaises (BadParameter ) as exception , patch ("pathlib.Path.is_file" , new = mock_is_file ), patch (
214250 "pathlib.Path.read_text" , new = mock_read_text
215251 ):
216- self .param_type .convert (f "file://A.yaml" , None , MagicMock ())
252+ self .param_type .convert ("file://A.yaml" , None , MagicMock ())
217253
218254 self .assertIn ("Infinite recursion detected in file references" , str (exception .exception ))
219255
256+ @parameterized .expand (
257+ [
258+ (
259+ {
260+ "config/default.yaml" : "Base: default\n Env: dev" ,
261+ "config/prod.yaml" : "- file://default.yaml\n - Env: production\n - Region: us-east-1" ,
262+ },
263+ "config/prod.yaml" ,
264+ {"Base" : "default" , "Env" : "production" , "Region" : "us-east-1" },
265+ ),
266+ (
267+ {
268+ "config/shared/common.yaml" : "Common: shared" ,
269+ "config/prod.yaml" : "- file://shared/common.yaml\n - Env: prod" ,
270+ },
271+ "config/prod.yaml" ,
272+ {"Common" : "shared" , "Env" : "prod" },
273+ ),
274+ ]
275+ )
276+ def test_relative_path_nested_includes (self , file_contents , entry_file , expected ):
277+ """Test that nested files can reference other files using relative paths"""
278+ from pathlib import Path
279+ import tempfile
280+
281+ with tempfile .TemporaryDirectory () as temp :
282+ temp_path = Path (temp )
283+
284+ # Create all files
285+ for file_path , content in file_contents .items ():
286+ full_path = temp_path / file_path
287+ full_path .parent .mkdir (parents = True , exist_ok = True )
288+ full_path .write_text (content )
289+
290+ entry_path = temp_path / entry_file
291+ result = self .param_type .convert (f"file://{ entry_path } " , None , MagicMock ())
292+ self .assertEqual (result , expected )
293+
294+ def test_include_key_invalid_type (self ):
295+ """Test that $include with invalid type fails with clear error"""
296+ mock_files = {
297+ "invalid.yaml" : "$include: 123\n Env: prod" ,
298+ }
299+
300+ def mock_read_text (file_path ):
301+ return mock_files .get (file_path .name , "" )
302+
303+ def mock_is_file (file_path ):
304+ return file_path .name in mock_files
305+
306+ with self .assertRaises (BadParameter ) as exception , patch ("pathlib.Path.is_file" , new = mock_is_file ), patch (
307+ "pathlib.Path.read_text" , new = mock_read_text
308+ ):
309+ self .param_type .convert ("file://invalid.yaml" , None , MagicMock ())
310+
311+ self .assertIn ("$include must be a string or list of strings" , str (exception .exception ))
312+
220313
221314class TestCfnMetadataType (TestCase ):
222315 def setUp (self ):
0 commit comments