Skip to content

Commit 314da60

Browse files
fix: address review issues from formal review of all 18 review-sets
Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/d0aab0b1-f98b-471e-b038-8888d3cb00f5 Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
1 parent b3a14a1 commit 314da60

10 files changed

Lines changed: 162 additions & 6 deletions

File tree

docs/design/file-assert/utilities/path-helpers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ the base directory.
2020
**Validation steps:**
2121

2222
1. Reject null inputs via `ArgumentNullException.ThrowIfNull`.
23-
2. Reject `relativePath` values that contain `..` (path traversal).
23+
2. Reject `relativePath` values that contain `..` as a path component (path traversal).
2424
3. Reject `relativePath` values that are rooted (absolute paths).
2525
4. Combine the paths with `Path.Combine`.
2626
5. Compute the full (canonical) paths of both base and combined paths.

docs/reqstream/file-assert/configuration/configuration.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ sections:
2828
that filtering works correctly across the full load-and-run pipeline.
2929
tests:
3030
- ConfigurationSubsystem_RunWithFilter_ExecutesOnlyMatchingTests
31+
- ConfigurationSubsystem_RunWithTagFilter_ExecutesOnlyMatchingTests

docs/reqstream/file-assert/modeling/file-assert-file.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ sections:
1919
- FileAssertFile_Create_ValidData_CreatesFile
2020
- FileAssertFile_Create_NullData_ThrowsArgumentNullException
2121
- FileAssertFile_Create_NullPattern_ThrowsInvalidOperationException
22+
- FileAssertFile_Create_BlankPattern_ThrowsInvalidOperationException
2223

2324
- id: FileAssert-FileAssertFile-CountConstraints
2425
title: The FileAssertFile class shall enforce optional minimum and maximum file count constraints.
@@ -42,6 +43,7 @@ sections:
4243
tests:
4344
- FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError
4445
- FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError
46+
- FileAssertFile_Run_MultipleFiles_OneFailsContentRule_WritesError
4547

4648
- id: FileAssert-FileAssertFile-ExactCount
4749
title: The FileAssertFile class shall enforce an exact file count constraint when declared.
@@ -66,3 +68,4 @@ sections:
6668
tests:
6769
- FileAssertFile_Run_TooSmall_WritesError
6870
- FileAssertFile_Run_TooLarge_WritesError
71+
- FileAssertFile_Run_MultipleFiles_OneViolatesSizeConstraint_WritesError

docs/reqstream/file-assert/selftest/selftest.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ sections:
1717
output all work together correctly.
1818
tests:
1919
- SelfTestSubsystem_Run_ExecutesBuiltInTestsAndProducesSummary
20+
21+
- id: FileAssert-SelfTestSubsystem-ResultsSerialization
22+
title: The SelfTest subsystem shall write validation results to a file in TRX or JUnit XML format when requested.
23+
justification: |
24+
CI/CD pipelines and regulated environments require machine-readable test result files
25+
to collect evidence that self-validation passed. Supporting both TRX and JUnit XML
26+
formats ensures compatibility with common test reporting tools.
27+
tests:
28+
- Validation_Run_WithTrxResultsFile_WritesTrxFile
29+
- Validation_Run_WithXmlResultsFile_WritesXmlFile

docs/reqstream/file-assert/utilities/path-helpers.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ sections:
2222
- PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly
2323
- PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly
2424
- PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath
25+
- PathHelpers_SafePathCombine_DoubleDotInFilename_CombinesCorrectly
2526

2627
- id: FileAssert-PathHelpers-NullValidation
2728
title: The PathHelpers class shall reject null base or relative path arguments.

src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,13 @@ internal static string SafePathCombine(string basePath, string relativePath)
3838
ArgumentNullException.ThrowIfNull(basePath);
3939
ArgumentNullException.ThrowIfNull(relativePath);
4040

41-
// Ensure the relative path doesn't contain path traversal sequences
42-
if (relativePath.Contains("..") || Path.IsPathRooted(relativePath))
41+
// Ensure the relative path doesn't contain path traversal sequences.
42+
// Split by directory separators and check each component to avoid false positives
43+
// for filenames that contain ".." as a substring (e.g. "my..file.txt").
44+
var pathComponents = relativePath.Split(
45+
[Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar],
46+
StringSplitOptions.None);
47+
if (pathComponents.Any(c => c == "..") || Path.IsPathRooted(relativePath))
4348
{
4449
throw new ArgumentException($"Invalid path component: {relativePath}", nameof(relativePath));
4550
}

test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationSubsystemTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,50 @@ public void ConfigurationSubsystem_RunWithFilter_ExecutesOnlyMatchingTests()
115115
tempDir.Delete(recursive: true);
116116
}
117117
}
118+
119+
/// <summary>
120+
/// Verifies that the Configuration subsystem executes only tests whose tag matches
121+
/// the provided filter when running a configuration with multiple tests.
122+
/// </summary>
123+
[TestMethod]
124+
public void ConfigurationSubsystem_RunWithTagFilter_ExecutesOnlyMatchingTests()
125+
{
126+
// Arrange - two tests with different tags; only one file exists so only that test passes
127+
var tempDir = Directory.CreateTempSubdirectory("fileassert_config_");
128+
try
129+
{
130+
var configPath = Path.Combine(tempDir.FullName, "config.yaml");
131+
File.WriteAllText(configPath, """
132+
tests:
133+
- name: "Alpha"
134+
tags:
135+
- smoke
136+
files:
137+
- pattern: "alpha.txt"
138+
min: 1
139+
- name: "Beta"
140+
tags:
141+
- regression
142+
files:
143+
- pattern: "beta.txt"
144+
min: 1
145+
""");
146+
147+
// Create only alpha.txt so the Alpha test passes and Beta would fail
148+
File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "content");
149+
150+
var config = FileAssertConfig.ReadFromFile(configPath);
151+
using var context = Context.Create(["--silent"]);
152+
153+
// Act - run with the "smoke" tag filter only
154+
config.Run(context, ["smoke"]);
155+
156+
// Assert - no errors because only Alpha ran (matching the smoke tag) and alpha.txt exists
157+
Assert.AreEqual(0, context.ExitCode);
158+
}
159+
finally
160+
{
161+
tempDir.Delete(recursive: true);
162+
}
163+
}
118164
}

test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,14 @@ public void IntegrationTest_SilentFlag_SuppressesOutput()
148148
{
149149
// Act
150150
var exitCode = Runner.Run(
151-
out var _,
151+
out var output,
152152
"dotnet",
153153
_dllPath,
154154
"--silent");
155155

156156
// Assert
157157
Assert.AreEqual(0, exitCode);
158-
159-
// Output check removed since silent mode may still produce some output
158+
Assert.AreEqual(string.Empty, output);
160159
}
161160

162161
/// <summary>

test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ public void FileAssertFile_Create_NullPattern_ThrowsInvalidOperationException()
7373
Assert.Contains("pattern", exception.Message);
7474
}
7575

76+
/// <summary>
77+
/// Verifies that Create throws <see cref="InvalidOperationException"/> when Pattern is blank.
78+
/// </summary>
79+
[TestMethod]
80+
public void FileAssertFile_Create_BlankPattern_ThrowsInvalidOperationException()
81+
{
82+
// Arrange
83+
var data = new FileAssertFileData { Pattern = " " };
84+
85+
// Act & Assert
86+
var exception = Assert.Throws<InvalidOperationException>(() => FileAssertFile.Create(data));
87+
Assert.Contains("pattern", exception.Message);
88+
}
89+
7690
/// <summary>
7791
/// Verifies that Run produces no error when there are no matching files and no constraints.
7892
/// </summary>
@@ -323,4 +337,64 @@ public void FileAssertFile_Run_TooLarge_WritesError()
323337
tempDir.Delete(recursive: true);
324338
}
325339
}
340+
341+
/// <summary>
342+
/// Verifies that Run checks size constraints against every matched file, not just the first.
343+
/// </summary>
344+
[TestMethod]
345+
public void FileAssertFile_Run_MultipleFiles_OneViolatesSizeConstraint_WritesError()
346+
{
347+
// Arrange - create two files: one large enough, one too small
348+
var tempDir = Directory.CreateTempSubdirectory("fileassert_test_");
349+
try
350+
{
351+
File.WriteAllText(Path.Combine(tempDir.FullName, "ok.txt"), "enough content here");
352+
File.WriteAllText(Path.Combine(tempDir.FullName, "small.txt"), string.Empty);
353+
var data = new FileAssertFileData { Pattern = "*.txt", MinSize = 5 };
354+
var file = FileAssertFile.Create(data);
355+
using var context = Context.Create(["--silent"]);
356+
357+
// Act
358+
file.Run(context, tempDir.FullName);
359+
360+
// Assert - the small file should trigger an error
361+
Assert.AreEqual(1, context.ExitCode);
362+
}
363+
finally
364+
{
365+
tempDir.Delete(recursive: true);
366+
}
367+
}
368+
369+
/// <summary>
370+
/// Verifies that Run applies content rules to every matched file, not just the first.
371+
/// </summary>
372+
[TestMethod]
373+
public void FileAssertFile_Run_MultipleFiles_OneFailsContentRule_WritesError()
374+
{
375+
// Arrange - create two files: one with required content, one without
376+
var tempDir = Directory.CreateTempSubdirectory("fileassert_test_");
377+
try
378+
{
379+
File.WriteAllText(Path.Combine(tempDir.FullName, "good.txt"), "expected content here");
380+
File.WriteAllText(Path.Combine(tempDir.FullName, "bad.txt"), "unrelated content");
381+
var data = new FileAssertFileData
382+
{
383+
Pattern = "*.txt",
384+
Rules = [new FileAssertRuleData { Contains = "expected content" }]
385+
};
386+
var file = FileAssertFile.Create(data);
387+
using var context = Context.Create(["--silent"]);
388+
389+
// Act
390+
file.Run(context, tempDir.FullName);
391+
392+
// Assert - the bad file should trigger an error
393+
Assert.AreEqual(1, context.ExitCode);
394+
}
395+
finally
396+
{
397+
tempDir.Delete(recursive: true);
398+
}
399+
}
326400
}

test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ public void PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath()
152152
Assert.AreEqual(Path.Combine(basePath, relativePath), result);
153153
}
154154

155+
/// <summary>
156+
/// Test that SafePathCombine allows filenames that contain ".." as a substring but not as a path component.
157+
/// </summary>
158+
[TestMethod]
159+
public void PathHelpers_SafePathCombine_DoubleDotInFilename_CombinesCorrectly()
160+
{
161+
// Arrange - filename with ".." as substring, not a path traversal component
162+
var basePath = "/home/user/project";
163+
var relativePath = "my..file.txt";
164+
165+
// Act
166+
var result = PathHelpers.SafePathCombine(basePath, relativePath);
167+
168+
// Assert
169+
Assert.AreEqual(Path.Combine(basePath, relativePath), result);
170+
}
171+
155172
/// <summary>
156173
/// Test that SafePathCombine throws ArgumentNullException when base path is null.
157174
/// </summary>

0 commit comments

Comments
 (0)