diff --git a/NEW/JdeScoping.slnx b/NEW/JdeScoping.slnx
index de0edba..0be7837 100644
--- a/NEW/JdeScoping.slnx
+++ b/NEW/JdeScoping.slnx
@@ -11,6 +11,9 @@
+
+
+
@@ -23,5 +26,6 @@
+
diff --git a/NEW/src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs b/NEW/src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs
index 64b0ca7..9d8ad5d 100644
--- a/NEW/src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs
+++ b/NEW/src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs
@@ -176,8 +176,17 @@ public class CriteriaSheetGenerator
}
var options = _options.Value;
- var timezone = TimeZoneInfo.FindSystemTimeZoneById(options.TimezoneId);
- var localTime = TimeZoneInfo.ConvertTimeFromUtc(dateTime.Value, timezone);
+ var targetTimezone = TimeZoneInfo.FindSystemTimeZoneById(options.TimezoneId);
+ var dt = dateTime.Value;
+
+ // Convert to target timezone based on the source DateTime's Kind
+ var localTime = dt.Kind switch
+ {
+ DateTimeKind.Utc => TimeZoneInfo.ConvertTimeFromUtc(dt, targetTimezone),
+ DateTimeKind.Local => TimeZoneInfo.ConvertTime(dt, TimeZoneInfo.Local, targetTimezone),
+ // Unspecified - database values are stored in target timezone, no conversion needed
+ _ => dt
+ };
return $"{localTime:MMM dd, yyyy hh:mm:ss tt} {options.TimezoneAbbreviation}";
}
diff --git a/NEW/tests/JdeScoping.Api.IntegrationTests/TestWebApplicationFactory.cs b/NEW/tests/JdeScoping.Api.IntegrationTests/TestWebApplicationFactory.cs
index 2dd7582..f976b8b 100644
--- a/NEW/tests/JdeScoping.Api.IntegrationTests/TestWebApplicationFactory.cs
+++ b/NEW/tests/JdeScoping.Api.IntegrationTests/TestWebApplicationFactory.cs
@@ -53,12 +53,12 @@ public class TestWebApplicationFactory : WebApplicationFactory
// Ensure fake auth is used for tests
var authServiceDescriptor = services.SingleOrDefault(
- d => d.ServiceType == typeof(IAuthService));
+ d => d.ServiceType == typeof(IAuthenticationService));
if (authServiceDescriptor != null)
{
services.Remove(authServiceDescriptor);
}
- services.AddSingleton();
+ services.AddSingleton();
});
}
diff --git a/NEW/tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs b/NEW/tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs
index 4aad741..57e3517 100644
--- a/NEW/tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs
+++ b/NEW/tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs
@@ -1,5 +1,6 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Parsing;
+using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
@@ -7,7 +8,7 @@ namespace JdeScoping.ExcelIO.Tests.Parsing;
public class ExcelParserServiceTests
{
- private readonly ExcelParserService _service = new();
+ private readonly ExcelParserService _service = new(NullLogger.Instance);
[Fact]
public void ParseWorkOrders_ReturnsWorkOrderNumbers()
diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/JdeScoping.SecureStoreManager.Tests.csproj b/NEW/tests/JdeScoping.SecureStoreManager.Tests/JdeScoping.SecureStoreManager.Tests.csproj
index bfcc965..7a4b7b4 100644
--- a/NEW/tests/JdeScoping.SecureStoreManager.Tests/JdeScoping.SecureStoreManager.Tests.csproj
+++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/JdeScoping.SecureStoreManager.Tests.csproj
@@ -6,6 +6,8 @@
false
+
+
diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/TestAppBuilder.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/TestAppBuilder.cs
new file mode 100644
index 0000000..924cc4a
--- /dev/null
+++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/TestAppBuilder.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Headless;
+using JdeScoping.SecureStoreManager;
+
+[assembly: AvaloniaTestApplication(typeof(JdeScoping.SecureStoreManager.Tests.TestAppBuilder))]
+
+namespace JdeScoping.SecureStoreManager.Tests;
+
+public class TestAppBuilder
+{
+ public static AppBuilder BuildAvaloniaApp() =>
+ AppBuilder.Configure()
+ .UseHeadless(new AvaloniaHeadlessPlatformOptions());
+}
diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/DialogViewModelTests.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/DialogViewModelTests.cs
new file mode 100644
index 0000000..2623e79
--- /dev/null
+++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/DialogViewModelTests.cs
@@ -0,0 +1,535 @@
+using Shouldly;
+using Xunit;
+using JdeScoping.SecureStoreManager.ViewModels;
+
+namespace JdeScoping.SecureStoreManager.Tests.ViewModels;
+
+public class NewStoreDialogViewModelTests
+{
+ [Fact]
+ public void IsValid_WhenStorePathEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = string.Empty;
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = "/path/to/key.key";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenUseKeyFileButKeyFilePathEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenUsePasswordButPasswordEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenPasswordsDoNotMatch_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = "password123";
+ sut.ConfirmPassword = "different456";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WithValidKeyFileConfiguration_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = "/path/to/key.key";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsValid_WithValidPasswordConfiguration_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = "password123";
+ sut.ConfirmPassword = "password123";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void UseKeyFile_WhenSetToTrue_SetsUsePasswordToFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.UsePassword = true;
+
+ // Act
+ sut.UseKeyFile = true;
+
+ // Assert
+ sut.UsePassword.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void UsePassword_WhenSetToTrue_SetsUseKeyFileToFalse()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.UseKeyFile = true;
+
+ // Act
+ sut.UsePassword = true;
+
+ // Assert
+ sut.UseKeyFile.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void ValidationError_WhenStorePathEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Store path is required.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenKeyFilePathEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Key file path is required.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenPasswordEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Password is required.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenPasswordsDoNotMatch_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = "password123";
+ sut.ConfirmPassword = "different456";
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Passwords do not match.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenValid_ReturnsNull()
+ {
+ // Arrange
+ var sut = new NewStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = "/path/to/key.key";
+
+ // Act & Assert
+ sut.ValidationError.ShouldBeNull();
+ }
+}
+
+public class OpenStoreDialogViewModelTests
+{
+ [Fact]
+ public void IsValid_WhenStorePathEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenUseKeyFileButKeyFilePathEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenUsePasswordButPasswordEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WithValidKeyFileConfiguration_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = "/path/to/key.key";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsValid_WithValidPasswordConfiguration_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = "/path/to/store.json";
+ sut.UsePassword = true;
+ sut.Password = "password123";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void UseKeyFile_WhenSetToTrue_SetsUsePasswordToFalse()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.UsePassword = true;
+
+ // Act
+ sut.UseKeyFile = true;
+
+ // Assert
+ sut.UsePassword.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void UsePassword_WhenSetToTrue_SetsUseKeyFileToFalse()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.UseKeyFile = true;
+
+ // Act
+ sut.UsePassword = true;
+
+ // Assert
+ sut.UseKeyFile.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void ValidationError_WhenStorePathEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Store path is required.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenStoreFileDoesNotExist_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = "/nonexistent/path/to/store.json";
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Store file does not exist.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenKeyFilePathEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange - Create temp store file so we get past that validation
+ var tempFile = Path.GetTempFileName();
+ try
+ {
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = tempFile;
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Key file path is required.");
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Fact]
+ public void ValidationError_WhenKeyFileDoesNotExist_ReturnsAppropriateMessage()
+ {
+ // Arrange - Create temp store file so we get past that validation
+ var tempFile = Path.GetTempFileName();
+ try
+ {
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = tempFile;
+ sut.UseKeyFile = true;
+ sut.KeyFilePath = "/nonexistent/key.key";
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Key file does not exist.");
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Fact]
+ public void ValidationError_WhenPasswordEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange - Create temp store file so we get past that validation
+ var tempFile = Path.GetTempFileName();
+ try
+ {
+ var sut = new OpenStoreDialogViewModel();
+ sut.StorePath = tempFile;
+ sut.UsePassword = true;
+ sut.Password = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Password is required.");
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+}
+
+public class SecretEditDialogViewModelTests
+{
+ [Fact]
+ public void DefaultConstructor_SetsIsNewSecretToTrue()
+ {
+ // Arrange & Act
+ var sut = new SecretEditDialogViewModel();
+
+ // Assert
+ sut.IsNewSecret.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void ParameterizedConstructor_SetsKey()
+ {
+ // Arrange & Act
+ var sut = new SecretEditDialogViewModel("testKey", "testValue");
+
+ // Assert
+ sut.Key.ShouldBe("testKey");
+ }
+
+ [Fact]
+ public void ParameterizedConstructor_SetsValue()
+ {
+ // Arrange & Act
+ var sut = new SecretEditDialogViewModel("testKey", "testValue");
+
+ // Assert
+ sut.Value.ShouldBe("testValue");
+ }
+
+ [Fact]
+ public void ParameterizedConstructor_SetsIsNewSecretToFalse()
+ {
+ // Arrange & Act
+ var sut = new SecretEditDialogViewModel("testKey", "testValue");
+
+ // Assert
+ sut.IsNewSecret.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsKeyEditable_WhenIsNewSecret_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+
+ // Act & Assert
+ sut.IsKeyEditable.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsKeyEditable_WhenEditingExisting_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel("testKey", "testValue");
+
+ // Act & Assert
+ sut.IsKeyEditable.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void DialogTitle_WhenIsNewSecret_ReturnsAddSecret()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+
+ // Act & Assert
+ sut.DialogTitle.ShouldBe("Add Secret");
+ }
+
+ [Fact]
+ public void DialogTitle_WhenEditingExisting_ReturnsEditSecret()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel("testKey", "testValue");
+
+ // Act & Assert
+ sut.DialogTitle.ShouldBe("Edit Secret");
+ }
+
+ [Fact]
+ public void IsValid_WhenKeyEmpty_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ sut.Key = string.Empty;
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenKeyWhitespace_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ sut.Key = " ";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsValid_WhenKeyProvided_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ sut.Key = "validKey";
+
+ // Act & Assert
+ sut.IsValid.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void ValidationError_WhenKeyEmpty_ReturnsAppropriateMessage()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ sut.Key = string.Empty;
+
+ // Act & Assert
+ sut.ValidationError.ShouldBe("Key is required.");
+ }
+
+ [Fact]
+ public void ValidationError_WhenKeyProvided_ReturnsNull()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ sut.Key = "validKey";
+
+ // Act & Assert
+ sut.ValidationError.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Key_SetterRaisesPropertyChanged()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ var propertyChangedRaised = false;
+ sut.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == nameof(sut.Key))
+ propertyChangedRaised = true;
+ };
+
+ // Act
+ sut.Key = "newKey";
+
+ // Assert
+ propertyChangedRaised.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Value_SetterRaisesPropertyChanged()
+ {
+ // Arrange
+ var sut = new SecretEditDialogViewModel();
+ var propertyChangedRaised = false;
+ sut.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == nameof(sut.Value))
+ propertyChangedRaised = true;
+ };
+
+ // Act
+ sut.Value = "newValue";
+
+ // Assert
+ propertyChangedRaised.ShouldBeTrue();
+ }
+}
diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/RelayCommandTests.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/RelayCommandTests.cs
new file mode 100644
index 0000000..31ed854
--- /dev/null
+++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/RelayCommandTests.cs
@@ -0,0 +1,127 @@
+using Shouldly;
+using Xunit;
+using JdeScoping.SecureStoreManager.ViewModels;
+
+namespace JdeScoping.SecureStoreManager.Tests.ViewModels;
+
+public class RelayCommandTests
+{
+ [Fact]
+ public void Execute_CallsAction()
+ {
+ // Arrange
+ var wasCalled = false;
+ var sut = new RelayCommand(() => wasCalled = true);
+
+ // Act
+ sut.Execute(null);
+
+ // Assert
+ wasCalled.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Execute_PassesParameterToAction()
+ {
+ // Arrange
+ object? receivedParameter = null;
+ var sut = new RelayCommand(p => receivedParameter = p);
+
+ // Act
+ sut.Execute("testParam");
+
+ // Assert
+ receivedParameter.ShouldBe("testParam");
+ }
+
+ [Fact]
+ public void CanExecute_WhenNoPredicate_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new RelayCommand(() => { });
+
+ // Act
+ var result = sut.CanExecute(null);
+
+ // Assert
+ result.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void CanExecute_WhenPredicateReturnsTrue_ReturnsTrue()
+ {
+ // Arrange
+ var sut = new RelayCommand(() => { }, () => true);
+
+ // Act
+ var result = sut.CanExecute(null);
+
+ // Assert
+ result.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void CanExecute_WhenPredicateReturnsFalse_ReturnsFalse()
+ {
+ // Arrange
+ var sut = new RelayCommand(() => { }, () => false);
+
+ // Act
+ var result = sut.CanExecute(null);
+
+ // Assert
+ result.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void CanExecute_WithParameterPredicate_EvaluatesParameter()
+ {
+ // Arrange
+ var sut = new RelayCommand(_ => { }, p => p is string s && s == "valid");
+
+ // Act
+ var validResult = sut.CanExecute("valid");
+ var invalidResult = sut.CanExecute("invalid");
+
+ // Assert
+ validResult.ShouldBeTrue();
+ invalidResult.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void RaiseCanExecuteChanged_FiresCanExecuteChangedEvent()
+ {
+ // Arrange
+ var sut = new RelayCommand(() => { });
+ var eventFired = false;
+ sut.CanExecuteChanged += (s, e) => eventFired = true;
+
+ // Act
+ sut.RaiseCanExecuteChanged();
+
+ // Assert
+ eventFired.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void RaiseCanExecuteChanged_EventSenderIsSut()
+ {
+ // Arrange
+ var sut = new RelayCommand(() => { });
+ object? eventSender = null;
+ sut.CanExecuteChanged += (s, e) => eventSender = s;
+
+ // Act
+ sut.RaiseCanExecuteChanged();
+
+ // Assert
+ eventSender.ShouldBe(sut);
+ }
+
+ [Fact]
+ public void Constructor_WithNullExecute_ThrowsArgumentNullException()
+ {
+ // Arrange & Act & Assert
+ Should.Throw(() => new RelayCommand((Action