diff --git a/test/DemaConsulting.BuildMark.Tests/PathHelpersTests.cs b/test/DemaConsulting.BuildMark.Tests/PathHelpersTests.cs
new file mode 100644
index 0000000..8983134
--- /dev/null
+++ b/test/DemaConsulting.BuildMark.Tests/PathHelpersTests.cs
@@ -0,0 +1,101 @@
+// Copyright (c) DEMA Consulting
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+namespace DemaConsulting.BuildMark.Tests;
+
+///
+/// Tests for the PathHelpers class.
+///
+[TestClass]
+public class PathHelpersTests
+{
+ ///
+ /// Test that SafePathCombine correctly combines valid paths.
+ ///
+ [TestMethod]
+ public void PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly()
+ {
+ // Arrange
+ var basePath = "/home/user/project";
+ var relativePath = "subfolder/file.txt";
+
+ // Act
+ var result = PathHelpers.SafePathCombine(basePath, relativePath);
+
+ // Assert
+ Assert.AreEqual(Path.Combine(basePath, relativePath), result);
+ }
+
+ ///
+ /// Test that SafePathCombine throws ArgumentException for path traversal with double dots.
+ ///
+ [TestMethod]
+ public void PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException()
+ {
+ // Arrange
+ var basePath = "/home/user/project";
+ var relativePath = "../etc/passwd";
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ PathHelpers.SafePathCombine(basePath, relativePath));
+ Assert.Contains("Invalid path component", exception.Message);
+ }
+
+ ///
+ /// Test that SafePathCombine throws ArgumentException for path with double dots in middle.
+ ///
+ [TestMethod]
+ public void PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException()
+ {
+ // Arrange
+ var basePath = "/home/user/project";
+ var relativePath = "subfolder/../../../etc/passwd";
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ PathHelpers.SafePathCombine(basePath, relativePath));
+ Assert.Contains("Invalid path component", exception.Message);
+ }
+
+ ///
+ /// Test that SafePathCombine throws ArgumentException for absolute paths.
+ ///
+ [TestMethod]
+ public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException()
+ {
+ // Test Unix absolute path
+ var unixBasePath = "/home/user/project";
+ var unixRelativePath = "/etc/passwd";
+ var unixException = Assert.Throws(() =>
+ PathHelpers.SafePathCombine(unixBasePath, unixRelativePath));
+ Assert.Contains("Invalid path component", unixException.Message);
+
+ // Test Windows absolute path (only on Windows since Windows paths may not be rooted on Unix)
+ if (OperatingSystem.IsWindows())
+ {
+ var windowsBasePath = "C:\\Users\\project";
+ var windowsRelativePath = "C:\\Windows\\System32\\file.txt";
+ var windowsException = Assert.Throws(() =>
+ PathHelpers.SafePathCombine(windowsBasePath, windowsRelativePath));
+ Assert.Contains("Invalid path component", windowsException.Message);
+ }
+ }
+}
diff --git a/test/DemaConsulting.BuildMark.Tests/ValidationTests.cs b/test/DemaConsulting.BuildMark.Tests/ValidationTests.cs
new file mode 100644
index 0000000..43b937c
--- /dev/null
+++ b/test/DemaConsulting.BuildMark.Tests/ValidationTests.cs
@@ -0,0 +1,236 @@
+// Copyright (c) DEMA Consulting
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using DemaConsulting.BuildMark.RepoConnectors;
+
+namespace DemaConsulting.BuildMark.Tests;
+
+///
+/// Tests for the Validation class.
+///
+[TestClass]
+public class ValidationTests
+{
+ ///
+ /// Test that Validation.Run writes TRX results file when specified.
+ ///
+ [TestMethod]
+ public void Validation_Run_WithTrxResultsFile_WritesTrxFile()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var trxFile = Path.Combine(tempDir, "results.trx");
+ var args = new[] { "--validate", "--results", trxFile };
+
+ StringWriter? outputWriter = null;
+ StringWriter? errorWriter = null;
+
+ try
+ {
+ // Capture console output
+ outputWriter = new StringWriter();
+ errorWriter = new StringWriter();
+ Console.SetOut(outputWriter);
+ Console.SetError(errorWriter);
+
+ // Act
+ using var context = Context.Create(args, () => new MockRepoConnector());
+ Validation.Run(context);
+
+ // Assert - Verify TRX file was created
+ Assert.IsTrue(File.Exists(trxFile), "TRX file should be created");
+
+ // Verify TRX file contains expected content
+ var trxContent = File.ReadAllText(trxFile);
+ Assert.Contains("TestRun", trxContent);
+ Assert.Contains("BuildMark Self-Validation", trxContent);
+ }
+ finally
+ {
+ // Restore console output
+ var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
+ Console.SetOut(standardOutput);
+ var standardError = new StreamWriter(Console.OpenStandardError()) { AutoFlush = true };
+ Console.SetError(standardError);
+
+ outputWriter?.Dispose();
+ errorWriter?.Dispose();
+ }
+ }
+ finally
+ {
+ // Cleanup
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ ///
+ /// Test that Validation.Run writes JUnit XML results file when specified.
+ ///
+ [TestMethod]
+ public void Validation_Run_WithXmlResultsFile_WritesJUnitFile()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var xmlFile = Path.Combine(tempDir, "results.xml");
+ var args = new[] { "--validate", "--results", xmlFile };
+
+ StringWriter? outputWriter = null;
+ StringWriter? errorWriter = null;
+
+ try
+ {
+ // Capture console output
+ outputWriter = new StringWriter();
+ errorWriter = new StringWriter();
+ Console.SetOut(outputWriter);
+ Console.SetError(errorWriter);
+
+ // Act
+ using var context = Context.Create(args, () => new MockRepoConnector());
+ Validation.Run(context);
+
+ // Assert - Verify XML file was created
+ Assert.IsTrue(File.Exists(xmlFile), "XML file should be created");
+
+ // Verify XML file contains expected content
+ var xmlContent = File.ReadAllText(xmlFile);
+ Assert.Contains("testsuites", xmlContent);
+ Assert.Contains("BuildMark Self-Validation", xmlContent);
+ }
+ finally
+ {
+ // Restore console output
+ var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
+ Console.SetOut(standardOutput);
+ var standardError = new StreamWriter(Console.OpenStandardError()) { AutoFlush = true };
+ Console.SetError(standardError);
+
+ outputWriter?.Dispose();
+ errorWriter?.Dispose();
+ }
+ }
+ finally
+ {
+ // Cleanup
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ ///
+ /// Test that Validation.Run handles unsupported results file extension.
+ ///
+ [TestMethod]
+ public void Validation_Run_WithUnsupportedResultsFileExtension_ShowsError()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var unsupportedFile = Path.Combine(tempDir, "results.json");
+ var args = new[] { "--validate", "--results", unsupportedFile };
+
+ StringWriter? outputWriter = null;
+
+ try
+ {
+ // Capture console output
+ outputWriter = new StringWriter();
+ Console.SetOut(outputWriter);
+
+ // Act
+ using var context = Context.Create(args, () => new MockRepoConnector());
+ Validation.Run(context);
+
+ // Assert - Verify error message in output (WriteError writes to Console.WriteLine)
+ var output = outputWriter.ToString();
+ Assert.Contains("Unsupported results file format", output);
+ }
+ finally
+ {
+ // Restore console output
+ var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
+ Console.SetOut(standardOutput);
+
+ outputWriter?.Dispose();
+ }
+ }
+ finally
+ {
+ // Cleanup
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ ///
+ /// Test that Validation.Run handles write failure for results file.
+ ///
+ [TestMethod]
+ public void Validation_Run_WithInvalidResultsFilePath_ShowsError()
+ {
+ // Arrange
+ var invalidPath = Path.Combine("/invalid_path_that_does_not_exist_12345678", "results.trx");
+ var args = new[] { "--validate", "--results", invalidPath };
+
+ StringWriter? outputWriter = null;
+
+ try
+ {
+ // Capture console output
+ outputWriter = new StringWriter();
+ Console.SetOut(outputWriter);
+
+ // Act
+ using var context = Context.Create(args, () => new MockRepoConnector());
+ Validation.Run(context);
+
+ // Assert - Verify error message in output (WriteError writes to Console.WriteLine)
+ var output = outputWriter.ToString();
+ Assert.Contains("Failed to write results file", output);
+ }
+ finally
+ {
+ // Restore console output
+ var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
+ Console.SetOut(standardOutput);
+
+ outputWriter?.Dispose();
+ }
+ }
+}