From 1c546c111a7ee1db4f8262bc865cfe253cba8250 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 19 Jan 2026 14:55:22 -0500 Subject: [PATCH] fix: resolve test failures from timezone conversion and interface rename - Fix CriteriaSheetGenerator.FormatTimestamp to handle all DateTimeKind values - Update TestWebApplicationFactory to use IAuthenticationService - Add logger parameter to ExcelParserServiceTests - Add SecureStoreManager to solution under /utils/ folder --- NEW/JdeScoping.slnx | 4 + .../Generators/CriteriaSheetGenerator.cs | 13 +- .../TestWebApplicationFactory.cs | 4 +- .../Parsing/ExcelParserServiceTests.cs | 3 +- ...JdeScoping.SecureStoreManager.Tests.csproj | 2 + .../TestAppBuilder.cs | 14 + .../ViewModels/DialogViewModelTests.cs | 535 ++++++++++++++++++ .../ViewModels/RelayCommandTests.cs | 127 +++++ .../ViewModels/SecretItemViewModelTests.cs | 167 ++++++ .../ViewModels/ViewModelBaseTests.cs | 184 ++++++ .../Views/MainWindowTests.cs | 133 +++++ .../Views/NewStoreDialogTests.cs | 93 +++ .../Views/SecretEditDialogTests.cs | 114 ++++ 13 files changed, 1388 insertions(+), 5 deletions(-) create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/TestAppBuilder.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/DialogViewModelTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/RelayCommandTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/SecretItemViewModelTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/ViewModelBaseTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/MainWindowTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/NewStoreDialogTests.cs create mode 100644 NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/SecretEditDialogTests.cs 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)null!)); + } +} diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/SecretItemViewModelTests.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/SecretItemViewModelTests.cs new file mode 100644 index 0000000..6624cbe --- /dev/null +++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/SecretItemViewModelTests.cs @@ -0,0 +1,167 @@ +using Shouldly; +using Xunit; +using JdeScoping.SecureStoreManager.ViewModels; + +namespace JdeScoping.SecureStoreManager.Tests.ViewModels; + +public class SecretItemViewModelTests +{ + [Fact] + public void Constructor_InitializesKey() + { + // Arrange & Act + var sut = new SecretItemViewModel("testKey", "testValue"); + + // Assert + sut.Key.ShouldBe("testKey"); + } + + [Fact] + public void Constructor_InitializesActualValue() + { + // Arrange & Act + var sut = new SecretItemViewModel("testKey", "testValue"); + + // Assert + sut.ActualValue.ShouldBe("testValue"); + } + + [Fact] + public void Constructor_InitializesIsValueVisibleToFalse() + { + // Arrange & Act + var sut = new SecretItemViewModel("testKey", "testValue"); + + // Assert + sut.IsValueVisible.ShouldBeFalse(); + } + + [Fact] + public void DisplayValue_WhenNotVisible_ReturnsMaskedValue() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + + // Act & Assert + sut.DisplayValue.ShouldBe("********"); + } + + [Fact] + public void DisplayValue_WhenVisible_ReturnsActualValue() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + + // Act + sut.IsValueVisible = true; + + // Assert + sut.DisplayValue.ShouldBe("testValue"); + } + + [Fact] + public void ToggleVisibilityCommand_TogglesIsValueVisible() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + sut.IsValueVisible.ShouldBeFalse(); + + // Act + sut.ToggleVisibilityCommand.Execute(null); + + // Assert + sut.IsValueVisible.ShouldBeTrue(); + } + + [Fact] + public void ToggleVisibilityCommand_TogglesBackToHidden() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + sut.IsValueVisible = true; + + // Act + sut.ToggleVisibilityCommand.Execute(null); + + // Assert + sut.IsValueVisible.ShouldBeFalse(); + } + + [Fact] + public async Task CopyToClipboardCommand_RaisesOnCopyToClipboardEvent() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "secretPassword"); + string? copiedValue = null; + sut.OnCopyToClipboard += value => + { + copiedValue = value; + return Task.CompletedTask; + }; + + // Act + sut.CopyToClipboardCommand.Execute(null); + + // Assert - need to wait for async handler + // Give the async void handler time to complete + await Task.Delay(100); + copiedValue.ShouldBe("secretPassword"); + } + + [Fact] + public void IsValueVisible_SetterRaisesPropertyChangedForIsValueVisible() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(sut.IsValueVisible)) + propertyChangedRaised = true; + }; + + // Act + sut.IsValueVisible = true; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void IsValueVisible_SetterRaisesPropertyChangedForDisplayValue() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(sut.DisplayValue)) + propertyChangedRaised = true; + }; + + // Act + sut.IsValueVisible = true; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void IsValueVisible_SetToSameValue_DoesNotRaisePropertyChanged() + { + // Arrange + var sut = new SecretItemViewModel("testKey", "testValue"); + sut.IsValueVisible = false; // Already false, but explicitly set + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + propertyChangedRaised = true; + }; + + // Act + sut.IsValueVisible = false; + + // Assert + propertyChangedRaised.ShouldBeFalse(); + } +} diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/ViewModelBaseTests.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/ViewModelBaseTests.cs new file mode 100644 index 0000000..c22ef22 --- /dev/null +++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/ViewModels/ViewModelBaseTests.cs @@ -0,0 +1,184 @@ +using Shouldly; +using Xunit; +using JdeScoping.SecureStoreManager.ViewModels; + +namespace JdeScoping.SecureStoreManager.Tests.ViewModels; + +public class ViewModelBaseTests +{ + /// + /// Test implementation of ViewModelBase for testing purposes. + /// + private class TestViewModel : ViewModelBase + { + private string _testProperty = string.Empty; + private int _testIntProperty; + + public string TestProperty + { + get => _testProperty; + set => SetProperty(ref _testProperty, value); + } + + public int TestIntProperty + { + get => _testIntProperty; + set => SetProperty(ref _testIntProperty, value); + } + + public void RaisePropertyChangedForTest(string propertyName) + { + OnPropertyChanged(propertyName); + } + } + + [Fact] + public void SetProperty_WhenValueUnchanged_ReturnsFalse() + { + // Arrange + var sut = new TestViewModel(); + sut.TestProperty = "initialValue"; + + // Act + sut.TestProperty = "initialValue"; // Same value + + // Assert - SetProperty should have returned false, but we can verify by checking no event was raised + // We test this indirectly through the property changed event not firing + } + + [Fact] + public void SetProperty_WhenValueUnchanged_DoesNotRaisePropertyChanged() + { + // Arrange + var sut = new TestViewModel(); + sut.TestProperty = "initialValue"; + var eventRaised = false; + sut.PropertyChanged += (s, e) => eventRaised = true; + + // Act + sut.TestProperty = "initialValue"; // Same value + + // Assert + eventRaised.ShouldBeFalse(); + } + + [Fact] + public void SetProperty_WhenValueChanged_ReturnsTrue() + { + // Arrange + var sut = new TestViewModel(); + + // Act + sut.TestProperty = "newValue"; + + // Assert - SetProperty returned true (verified by property change being made) + sut.TestProperty.ShouldBe("newValue"); + } + + [Fact] + public void SetProperty_WhenValueChanged_RaisesPropertyChanged() + { + // Arrange + var sut = new TestViewModel(); + var eventRaised = false; + sut.PropertyChanged += (s, e) => eventRaised = true; + + // Act + sut.TestProperty = "newValue"; + + // Assert + eventRaised.ShouldBeTrue(); + } + + [Fact] + public void SetProperty_RaisesEventWithCorrectPropertyName() + { + // Arrange + var sut = new TestViewModel(); + string? raisedPropertyName = null; + sut.PropertyChanged += (s, e) => raisedPropertyName = e.PropertyName; + + // Act + sut.TestProperty = "newValue"; + + // Assert + raisedPropertyName.ShouldBe(nameof(TestViewModel.TestProperty)); + } + + [Fact] + public void SetProperty_RaisesEventWithSenderAsThis() + { + // Arrange + var sut = new TestViewModel(); + object? eventSender = null; + sut.PropertyChanged += (s, e) => eventSender = s; + + // Act + sut.TestProperty = "newValue"; + + // Assert + eventSender.ShouldBe(sut); + } + + [Fact] + public void OnPropertyChanged_RaisesPropertyChangedEvent() + { + // Arrange + var sut = new TestViewModel(); + var eventRaised = false; + sut.PropertyChanged += (s, e) => eventRaised = true; + + // Act + sut.RaisePropertyChangedForTest("SomeProperty"); + + // Assert + eventRaised.ShouldBeTrue(); + } + + [Fact] + public void OnPropertyChanged_RaisesEventWithCorrectPropertyName() + { + // Arrange + var sut = new TestViewModel(); + string? raisedPropertyName = null; + sut.PropertyChanged += (s, e) => raisedPropertyName = e.PropertyName; + + // Act + sut.RaisePropertyChangedForTest("CustomProperty"); + + // Assert + raisedPropertyName.ShouldBe("CustomProperty"); + } + + [Fact] + public void SetProperty_WithValueType_WorksCorrectly() + { + // Arrange + var sut = new TestViewModel(); + var eventRaised = false; + sut.PropertyChanged += (s, e) => eventRaised = true; + + // Act + sut.TestIntProperty = 42; + + // Assert + sut.TestIntProperty.ShouldBe(42); + eventRaised.ShouldBeTrue(); + } + + [Fact] + public void SetProperty_WithSameValueType_DoesNotRaiseEvent() + { + // Arrange + var sut = new TestViewModel(); + sut.TestIntProperty = 42; + var eventRaised = false; + sut.PropertyChanged += (s, e) => eventRaised = true; + + // Act + sut.TestIntProperty = 42; // Same value + + // Assert + eventRaised.ShouldBeFalse(); + } +} diff --git a/NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/MainWindowTests.cs b/NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/MainWindowTests.cs new file mode 100644 index 0000000..645dd67 --- /dev/null +++ b/NEW/tests/JdeScoping.SecureStoreManager.Tests/Views/MainWindowTests.cs @@ -0,0 +1,133 @@ +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; +using NSubstitute; +using Shouldly; +using Xunit; +using JdeScoping.SecureStoreManager.Services; +using JdeScoping.SecureStoreManager.ViewModels; +using JdeScoping.SecureStoreManager.Views; + +namespace JdeScoping.SecureStoreManager.Tests.Views; + +public class MainWindowTests +{ + [AvaloniaFact] + public void MainWindow_ShowsWithCorrectDefaultTitle() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + window.Title.ShouldBe("SecureStore Manager"); + } + + [AvaloniaFact] + public void MainWindow_HasExpectedWidth() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + window.Width.ShouldBe(800); + } + + [AvaloniaFact] + public void MainWindow_HasExpectedHeight() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + window.Height.ShouldBe(500); + } + + [AvaloniaFact] + public void MainWindow_DataContextIsMainWindowViewModel() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + window.DataContext.ShouldBeOfType(); + } + + [AvaloniaFact] + public void MainWindow_ContainsSecretsDataGrid() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + var dataGrid = window.FindDescendantOfType(); + dataGrid.ShouldNotBeNull(); + } + + [AvaloniaFact] + public void MainWindow_ContainsMenuBar() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + var menu = window.FindDescendantOfType(); + menu.ShouldNotBeNull(); + } + + [AvaloniaFact] + public void MainWindow_ContainsToolbarButtons() + { + // Arrange & Act + var window = new MainWindow(); + window.Show(); + + // Assert + var buttons = window.GetVisualDescendants().OfType