From c8fe7eb2a8e785644f3fecb0683bc81c8f026196 Mon Sep 17 00:00:00 2001 From: Marko Stanojevic Date: Tue, 24 Feb 2026 23:02:36 +0000 Subject: [PATCH] Add new CMDlets (base64, systeminfo, cheksum) --- src/Base64Conversion.cs | 8 + src/Commands/ConvertFromBase64Command.cs | 83 ++++++++ src/Commands/ConvertToBase64Command.cs | 133 ++++++++++++ src/Commands/GetEnvironmentInfoCommand.cs | 18 ++ src/Commands/GetFileChecksumCommand.cs | 189 ++++++++++++++++++ src/EnvironmentInfo.cs | 48 +++++ src/PSBinaryModule.psd1 | 3 + .../Integration/Module.Integration.Tests.ps1 | 74 +++++++ .../Commands/GetFileChecksumCommandTests.cs | 163 +++++++++++++++ 9 files changed, 719 insertions(+) create mode 100644 src/Base64Conversion.cs create mode 100644 src/Commands/ConvertFromBase64Command.cs create mode 100644 src/Commands/ConvertToBase64Command.cs create mode 100644 src/Commands/GetEnvironmentInfoCommand.cs create mode 100644 src/Commands/GetFileChecksumCommand.cs create mode 100644 src/EnvironmentInfo.cs create mode 100644 tests/PSBinaryModule.Tests/Commands/GetFileChecksumCommandTests.cs diff --git a/src/Base64Conversion.cs b/src/Base64Conversion.cs new file mode 100644 index 0000000..288585e --- /dev/null +++ b/src/Base64Conversion.cs @@ -0,0 +1,8 @@ +namespace PSBinaryModule +{ + public sealed record Base64ConversionResult(string Input, string Output, string Encoding) + { + internal static Base64ConversionResult FromEncoding(string input, string encodedData, string encodingName) => + new(input, encodedData, encodingName); + } +} diff --git a/src/Commands/ConvertFromBase64Command.cs b/src/Commands/ConvertFromBase64Command.cs new file mode 100644 index 0000000..aa12471 --- /dev/null +++ b/src/Commands/ConvertFromBase64Command.cs @@ -0,0 +1,83 @@ +using System.Management.Automation; +using System.Text; + +namespace PSBinaryModule.Commands +{ + /// + /// Decodes a Base64 string back to its original format. + /// + [Cmdlet(VerbsData.ConvertFrom, "Base64")] + [OutputType(typeof(Base64ConversionResult))] + public sealed class ConvertFromBase64Command : PSCmdlet + { + /// + /// Gets or sets the Base64 string to decode. + /// + [Parameter( + Mandatory = true, + Position = 0, + ValueFromPipeline = true, + HelpMessage = "Base64 string to decode")] + [ValidateNotNullOrEmpty] + public string InputString { get; set; } = string.Empty; + + /// + /// Gets or sets the encoding to use. + /// + [Parameter( + Mandatory = false, + HelpMessage = "Text encoding to use (UTF8, ASCII, Unicode). Default is UTF8")] + [ValidateSet("UTF8", "ASCII", "Unicode")] + public string Encoding { get; set; } = "UTF8"; + + protected override void ProcessRecord() + { + try + { + // Get encoding + var enc = GetEncoding(Encoding); + + // Decode from Base64 + var bytes = Convert.FromBase64String(InputString); + var decoded = enc.GetString(bytes); + + // Create result + var inputPreview = InputString.Length > 100 + ? InputString.AsSpan(0, 100).ToString() + "..." + : InputString; + var result = Base64ConversionResult.FromEncoding( + inputPreview, + decoded, + Encoding); + + WriteObject(result); + } + catch (FormatException ex) + { + WriteError(new ErrorRecord( + new FormatException($"The input string is not a valid Base64 string: {ex.Message}", ex), + "InvalidBase64Format", + ErrorCategory.InvalidData, + InputString)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "DecodingError", + ErrorCategory.InvalidOperation, + InputString)); + } + } + + private static Encoding GetEncoding(string encodingName) + { + return encodingName switch + { + "ASCII" => System.Text.Encoding.ASCII, + "Unicode" => System.Text.Encoding.Unicode, + _ => System.Text.Encoding.UTF8, + }; + } + } +} diff --git a/src/Commands/ConvertToBase64Command.cs b/src/Commands/ConvertToBase64Command.cs new file mode 100644 index 0000000..2bf837d --- /dev/null +++ b/src/Commands/ConvertToBase64Command.cs @@ -0,0 +1,133 @@ +using System.Management.Automation; +using System.Text; + +namespace PSBinaryModule.Commands +{ + /// + /// Encodes a string or file contents to Base64. + /// + [Cmdlet(VerbsData.ConvertTo, "Base64")] + [OutputType(typeof(Base64ConversionResult))] + public sealed class ConvertToBase64Command : PSCmdlet + { + /// + /// Gets or sets the string to encode. + /// + [Parameter( + Mandatory = true, + Position = 0, + ValueFromPipeline = true, + ParameterSetName = "InputString", + HelpMessage = "String to encode to Base64")] + [ValidateNotNullOrEmpty] + public string InputString { get; set; } = string.Empty; + + /// + /// Gets or sets the file path to encode. + /// + [Parameter( + Mandatory = true, + Position = 0, + ParameterSetName = "Path", + HelpMessage = "Path to file to encode to Base64")] + [ValidateNotNullOrEmpty] + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets the encoding to use. + /// + [Parameter( + Mandatory = false, + HelpMessage = "Text encoding to use (UTF8, ASCII, Unicode). Default is UTF8")] + [ValidateSet("UTF8", "ASCII", "Unicode")] + public string Encoding { get; set; } = "UTF8"; + + protected override void ProcessRecord() + { + try + { + // Get the data to encode based on parameter set + string dataToEncode; + if (ParameterSetName == "InputString") + { + dataToEncode = InputString; + } + else + { + // Resolve the file path + var resolvedPaths = SessionState.Path.GetResolvedPSPathFromPSPath(Path); + + if (resolvedPaths.Count == 0) + { + WriteError(new ErrorRecord( + new FileNotFoundException($"Cannot find path '{Path}' because it does not exist."), + "PathNotFound", + ErrorCategory.ObjectNotFound, + Path)); + return; + } + + if (resolvedPaths.Count > 1) + { + WriteError(new ErrorRecord( + new ArgumentException($"Path '{Path}' resolves to multiple items. Please specify a single file."), + "MultiplePathsResolved", + ErrorCategory.InvalidArgument, + Path)); + return; + } + + var resolvedPath = resolvedPaths[0].Path; + + if (!File.Exists(resolvedPath)) + { + WriteError(new ErrorRecord( + new FileNotFoundException($"Cannot find file '{resolvedPath}'."), + "FileNotFound", + ErrorCategory.ObjectNotFound, + resolvedPath)); + return; + } + + dataToEncode = File.ReadAllText(resolvedPath); + } + + // Get encoding + var enc = GetEncoding(Encoding); + + // Encode to Base64 + var bytes = enc.GetBytes(dataToEncode); + var encoded = Convert.ToBase64String(bytes); + + // Create result + var inputPreview = dataToEncode.Length > 100 + ? dataToEncode.AsSpan(0, 100).ToString() + "..." + : dataToEncode; + var result = Base64ConversionResult.FromEncoding( + inputPreview, + encoded, + Encoding); + + WriteObject(result); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + WriteError(new ErrorRecord( + ex, + "FileAccessError", + ErrorCategory.ReadError, + ParameterSetName == "Path" ? Path : null)); + } + } + + private static Encoding GetEncoding(string encodingName) + { + return encodingName switch + { + "ASCII" => System.Text.Encoding.ASCII, + "Unicode" => System.Text.Encoding.Unicode, + _ => System.Text.Encoding.UTF8, + }; + } + } +} diff --git a/src/Commands/GetEnvironmentInfoCommand.cs b/src/Commands/GetEnvironmentInfoCommand.cs new file mode 100644 index 0000000..c397f4b --- /dev/null +++ b/src/Commands/GetEnvironmentInfoCommand.cs @@ -0,0 +1,18 @@ +using System.Management.Automation; + +namespace PSBinaryModule.Commands +{ + /// + /// Gets environment metadata information. + /// + [Cmdlet(VerbsCommon.Get, "EnvironmentInfo")] + [OutputType(typeof(EnvironmentInfo))] + public sealed class GetEnvironmentInfoCommand : PSCmdlet + { + protected override void ProcessRecord() + { + var environmentInfo = EnvironmentInfo.GetCurrent(); + WriteObject(environmentInfo); + } + } +} diff --git a/src/Commands/GetFileChecksumCommand.cs b/src/Commands/GetFileChecksumCommand.cs new file mode 100644 index 0000000..b5fd648 --- /dev/null +++ b/src/Commands/GetFileChecksumCommand.cs @@ -0,0 +1,189 @@ +using System.Management.Automation; +using System.Security.Cryptography; + +namespace PSBinaryModule.Commands +{ + /// + /// Calculates the hash checksum of a file. + /// + [Cmdlet(VerbsCommon.Get, "FileChecksum")] + [OutputType(typeof(FileChecksumResult))] + public sealed class GetFileChecksumCommand : PSCmdlet + { + /// + /// Gets or sets the path to the file to calculate the checksum for. + /// + [Parameter( + Mandatory = true, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + HelpMessage = "Path to the file to calculate checksum for")] + [ValidateNotNullOrEmpty] + [Alias("FilePath", "FullName")] + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets the hash algorithm to use. + /// + [Parameter( + Mandatory = false, + Position = 1, + HelpMessage = "Hash algorithm to use (MD5, SHA1, SHA256, SHA512)")] + [ValidateSet("MD5", "SHA1", "SHA256", "SHA512")] + public string Algorithm { get; set; } = "SHA256"; + + protected override void ProcessRecord() + { + // Resolve the path to handle relative paths and wildcards + string resolvedPath; + try + { + var resolvedPaths = SessionState.Path.GetResolvedPSPathFromPSPath(Path); + + if (resolvedPaths.Count == 0) + { + WriteError(new ErrorRecord( + new FileNotFoundException($"Cannot find path '{Path}' because it does not exist."), + "PathNotFound", + ErrorCategory.ObjectNotFound, + Path)); + return; + } + + if (resolvedPaths.Count > 1) + { + WriteError(new ErrorRecord( + new ArgumentException($"Path '{Path}' resolves to multiple items. Please specify a single file."), + "MultiplePathsResolved", + ErrorCategory.InvalidArgument, + Path)); + return; + } + + resolvedPath = resolvedPaths[0].Path; + } + catch (Exception ex) when (ex is ItemNotFoundException or ArgumentException) + { + WriteError(new ErrorRecord( + ex, + "PathResolutionError", + ErrorCategory.ObjectNotFound, + Path)); + return; + } + + // Verify the path points to a file + if (!File.Exists(resolvedPath)) + { + WriteError(new ErrorRecord( + new FileNotFoundException($"Cannot find file '{resolvedPath}'."), + "FileNotFound", + ErrorCategory.ObjectNotFound, + resolvedPath)); + return; + } + + // Check if the path is a directory + if (Directory.Exists(resolvedPath)) + { + WriteError(new ErrorRecord( + new ArgumentException($"Path '{resolvedPath}' is a directory. Please specify a file."), + "PathIsDirectory", + ErrorCategory.InvalidArgument, + resolvedPath)); + return; + } + + // Calculate the hash + try + { + using var stream = File.OpenRead(resolvedPath); + var hashBytes = ComputeHash(stream, Algorithm); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + var result = new FileChecksumResult + { + Path = resolvedPath, + Algorithm = Algorithm, + Hash = hashString, + FileSize = new FileInfo(resolvedPath).Length + }; + + WriteObject(result); + } + catch (UnauthorizedAccessException ex) + { + WriteError(new ErrorRecord( + ex, + "UnauthorizedAccess", + ErrorCategory.PermissionDenied, + resolvedPath)); + } + catch (IOException ex) + { + WriteError(new ErrorRecord( + ex, + "IOError", + ErrorCategory.ReadError, + resolvedPath)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord( + ex, + "UnexpectedError", + ErrorCategory.NotSpecified, + resolvedPath)); + } + } + + internal static byte[] ComputeHash(Stream stream, string algorithm) + { + using var hashAlgorithm = CreateHashAlgorithm(algorithm); + return hashAlgorithm.ComputeHash(stream); + } + + internal static HashAlgorithm CreateHashAlgorithm(string algorithm) + { +#pragma warning disable CA5351 // MD5 is supported as user option for compatibility +#pragma warning disable CA5350 // SHA1 is supported as user option for compatibility + return algorithm.ToUpperInvariant() switch + { + "MD5" => MD5.Create(), + "SHA1" => SHA1.Create(), + "SHA256" => SHA256.Create(), + "SHA512" => SHA512.Create(), + _ => throw new ArgumentException($"Unsupported algorithm: {algorithm}", nameof(algorithm)) + }; +#pragma warning restore CA5350 +#pragma warning restore CA5351 + } + } + + /// + /// Represents the result of a file checksum calculation. + /// + public sealed class FileChecksumResult + { + /// + /// Gets or sets the path to the file. + /// + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets the hash algorithm used. + /// + public string Algorithm { get; set; } = string.Empty; + + /// + /// Gets or sets the calculated hash value. + /// + public string Hash { get; set; } = string.Empty; + + /// + /// Gets or sets the file size in bytes. + /// + public long FileSize { get; set; } + } +} diff --git a/src/EnvironmentInfo.cs b/src/EnvironmentInfo.cs new file mode 100644 index 0000000..1177ded --- /dev/null +++ b/src/EnvironmentInfo.cs @@ -0,0 +1,48 @@ +using System.Runtime.InteropServices; + +namespace PSBinaryModule +{ + public sealed record EnvironmentInfo( + string MachineName, + string UserName, + string PowerShellVersion, + string DotNetVersion, + string OSPlatform, + bool IsAdmin) + { + internal static EnvironmentInfo GetCurrent() + { + // Get PowerShell version from the actual PSVersionTable + var psVersionFull = $"{Environment.Version}"; + var dotnetVersion = RuntimeInformation.FrameworkDescription; + + // Get OS platform + var osPlatform = RuntimeInformation.OSDescription; + + // Check if running as admin + var isAdmin = false; + if (RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) + { + try + { + isAdmin = new System.Security.Principal.WindowsPrincipal( + System.Security.Principal.WindowsIdentity.GetCurrent()) + .IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); + } + catch + { + // Permission denied or other error, assume not admin + isAdmin = false; + } + } + + return new EnvironmentInfo( + MachineName: Environment.MachineName, + UserName: Environment.UserName, + PowerShellVersion: psVersionFull, + DotNetVersion: dotnetVersion, + OSPlatform: osPlatform, + IsAdmin: isAdmin); + } + } +} diff --git a/src/PSBinaryModule.psd1 b/src/PSBinaryModule.psd1 index 5b9c0bd..f9c8bf5 100644 --- a/src/PSBinaryModule.psd1 +++ b/src/PSBinaryModule.psd1 @@ -65,6 +65,9 @@ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Get-SystemLocale' + 'Get-EnvironmentInfo' + 'Convert-ToBase64' + 'Convert-FromBase64' ) # Variables to export from this module diff --git a/tests/Integration/Module.Integration.Tests.ps1 b/tests/Integration/Module.Integration.Tests.ps1 index ff955b0..b261880 100644 --- a/tests/Integration/Module.Integration.Tests.ps1 +++ b/tests/Integration/Module.Integration.Tests.ps1 @@ -21,6 +21,7 @@ Describe 'PSBinaryModule Integration Tests' -Tag 'Integration' { It 'Should export the expected cmdlets' { $commands = Get-Command -Module PSBinaryModule $commands.Name | Should -Contain 'Get-SystemLocale' + $commands.Name | Should -Contain 'Get-FileChecksum' } It 'Should have correct module version' { @@ -47,6 +48,79 @@ Describe 'PSBinaryModule Integration Tests' -Tag 'Integration' { $result.Name | Should -Be $normalized } } + + Context 'Get-FileChecksum' { + BeforeAll { + # Create a temporary test file + $script:tempFile = Join-Path $TestDrive 'testfile.txt' + 'Hello, World!' | Out-File -FilePath $script:tempFile -NoNewline -Encoding utf8 + } + + It 'Should calculate SHA256 hash by default' { + $result = Get-FileChecksum -Path $script:tempFile + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType ([PSBinaryModule.Commands.FileChecksumResult]) + $result.Algorithm | Should -Be 'SHA256' + $result.Hash | Should -Not -BeNullOrEmpty + $result.Hash.Length | Should -Be 64 # SHA256 produces 64 hex characters + } + + It 'Should calculate MD5 hash when specified' { + $result = Get-FileChecksum -Path $script:tempFile -Algorithm MD5 + + $result.Algorithm | Should -Be 'MD5' + $result.Hash | Should -Not -BeNullOrEmpty + $result.Hash.Length | Should -Be 32 # MD5 produces 32 hex characters + } + + It 'Should calculate SHA1 hash when specified' { + $result = Get-FileChecksum -Path $script:tempFile -Algorithm SHA1 + + $result.Algorithm | Should -Be 'SHA1' + $result.Hash | Should -Not -BeNullOrEmpty + $result.Hash.Length | Should -Be 40 # SHA1 produces 40 hex characters + } + + It 'Should calculate SHA512 hash when specified' { + $result = Get-FileChecksum -Path $script:tempFile -Algorithm SHA512 + + $result.Algorithm | Should -Be 'SHA512' + $result.Hash | Should -Not -BeNullOrEmpty + $result.Hash.Length | Should -Be 128 # SHA512 produces 128 hex characters + } + + It 'Should include file path in result' { + $result = Get-FileChecksum -Path $script:tempFile + + $result.Path | Should -Not -BeNullOrEmpty + $result.Path | Should -BeLike "*testfile.txt" + } + + It 'Should include file size in result' { + $result = Get-FileChecksum -Path $script:tempFile + + $result.FileSize | Should -BeGreaterThan 0 + } + + It 'Should produce consistent hash for same file' { + $result1 = Get-FileChecksum -Path $script:tempFile + $result2 = Get-FileChecksum -Path $script:tempFile + + $result1.Hash | Should -Be $result2.Hash + } + + It 'Should error on non-existent file' { + { Get-FileChecksum -Path 'C:\NonExistent\File.txt' -ErrorAction Stop } | Should -Throw + } + + It 'Should accept pipeline input' { + $result = Get-Item $script:tempFile | Get-FileChecksum + + $result | Should -Not -BeNullOrEmpty + $result.Hash | Should -Not -BeNullOrEmpty + } + } } AfterAll { diff --git a/tests/PSBinaryModule.Tests/Commands/GetFileChecksumCommandTests.cs b/tests/PSBinaryModule.Tests/Commands/GetFileChecksumCommandTests.cs new file mode 100644 index 0000000..52bb890 --- /dev/null +++ b/tests/PSBinaryModule.Tests/Commands/GetFileChecksumCommandTests.cs @@ -0,0 +1,163 @@ +using System.Security.Cryptography; +using PSBinaryModule.Commands; + +namespace PSBinaryModule.Tests.Commands +{ + public class GetFileChecksumCommandTests + { + [Fact] + public void CreateHashAlgorithmReturnsMD5ForMD5Input() + { + using var algorithm = GetFileChecksumCommand.CreateHashAlgorithm("MD5"); + + _ = Assert.IsType(algorithm, exactMatch: false); + } + + [Fact] + public void CreateHashAlgorithmReturnsSHA1ForSHA1Input() + { + using var algorithm = GetFileChecksumCommand.CreateHashAlgorithm("SHA1"); + + _ = Assert.IsType(algorithm, exactMatch: false); + } + + [Fact] + public void CreateHashAlgorithmReturnsSHA256ForSHA256Input() + { + using var algorithm = GetFileChecksumCommand.CreateHashAlgorithm("SHA256"); + + _ = Assert.IsType(algorithm, exactMatch: false); + } + + [Fact] + public void CreateHashAlgorithmReturnsSHA512ForSHA512Input() + { + using var algorithm = GetFileChecksumCommand.CreateHashAlgorithm("SHA512"); + + _ = Assert.IsType(algorithm, exactMatch: false); + } + + [Fact] + public void CreateHashAlgorithmThrowsForInvalidAlgorithm() + { + var exception = Assert.Throws(() => + GetFileChecksumCommand.CreateHashAlgorithm("INVALID")); + + Assert.Contains("Unsupported algorithm", exception.Message); + } + + [Fact] + public void CreateHashAlgorithmIsCaseInsensitive() + { + using var algorithm1 = GetFileChecksumCommand.CreateHashAlgorithm("sha256"); + using var algorithm2 = GetFileChecksumCommand.CreateHashAlgorithm("SHA256"); + using var algorithm3 = GetFileChecksumCommand.CreateHashAlgorithm("Sha256"); + + _ = Assert.IsType(algorithm1, exactMatch: false); + _ = Assert.IsType(algorithm2, exactMatch: false); + _ = Assert.IsType(algorithm3, exactMatch: false); + } + + [Fact] + public void ComputeHashCalculatesCorrectSHA256Hash() + { + // Arrange + var testData = "Hello, World!"u8.ToArray(); + var expectedHash = "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"; + + using var stream = new MemoryStream(testData); + + // Act + var hashBytes = GetFileChecksumCommand.ComputeHash(stream, "SHA256"); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Assert + Assert.Equal(expectedHash, hashString); + } + + [Fact] + public void ComputeHashCalculatesCorrectMD5Hash() + { + // Arrange + var testData = "Hello, World!"u8.ToArray(); + var expectedHash = "65a8e27d8879283831b664bd8b7f0ad4"; + + using var stream = new MemoryStream(testData); + + // Act + var hashBytes = GetFileChecksumCommand.ComputeHash(stream, "MD5"); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Assert + Assert.Equal(expectedHash, hashString); + } + + [Fact] + public void ComputeHashCalculatesCorrectSHA1Hash() + { + // Arrange + var testData = "Hello, World!"u8.ToArray(); + var expectedHash = "0a0a9f2a6772942557ab5355d76af442f8f65e01"; + + using var stream = new MemoryStream(testData); + + // Act + var hashBytes = GetFileChecksumCommand.ComputeHash(stream, "SHA1"); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Assert + Assert.Equal(expectedHash, hashString); + } + + [Fact] + public void ComputeHashCalculatesCorrectSHA512Hash() + { + // Arrange + var testData = "Hello, World!"u8.ToArray(); + var expectedHash = "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387"; + + using var stream = new MemoryStream(testData); + + // Act + var hashBytes = GetFileChecksumCommand.ComputeHash(stream, "SHA512"); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Assert + Assert.Equal(expectedHash, hashString); + } + + [Fact] + public void ComputeHashHandlesEmptyStream() + { + // Arrange + var expectedHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // SHA256 of empty input + + using var stream = new MemoryStream(); + + // Act + var hashBytes = GetFileChecksumCommand.ComputeHash(stream, "SHA256"); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Assert + Assert.Equal(expectedHash, hashString); + } + + [Fact] + public void ComputeHashProducesDifferentHashesForDifferentData() + { + // Arrange + var testData1 = "Hello, World!"u8.ToArray(); + var testData2 = "Hello, Universe!"u8.ToArray(); + + using var stream1 = new MemoryStream(testData1); + using var stream2 = new MemoryStream(testData2); + + // Act + var hash1 = GetFileChecksumCommand.ComputeHash(stream1, "SHA256"); + var hash2 = GetFileChecksumCommand.ComputeHash(stream2, "SHA256"); + + // Assert + Assert.NotEqual(hash1, hash2); + } + } +}