refactor(configmanager): rename UI project and split test projects

Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project
structure, and split the monolithic test project into Core.Tests,
Cli.Tests, and Ui.Tests to align with the source project organization.
This commit is contained in:
Joseph Doherty
2026-01-28 10:24:36 -05:00
parent 7c4781dfe3
commit 1fc7792cd1
131 changed files with 267 additions and 212 deletions
@@ -0,0 +1,241 @@
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Dialogs;
public class DiffPreviewDialogViewModelTests
{
[Fact]
public void Constructor_WithEmptyDiff_InitializesCorrectly()
{
// Arrange
var diff = new DiffResult
{
HasChanges = false,
Lines = [],
Insertions = 0,
Deletions = 0
};
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Lines.Count.ShouldBe(0);
sut.HasChanges.ShouldBeFalse();
sut.Insertions.ShouldBe(0);
sut.Deletions.ShouldBe(0);
sut.Result.ShouldBeFalse();
}
[Fact]
public void Constructor_WithChanges_PopulatesLines()
{
// Arrange
var diff = new DiffResult
{
HasChanges = true,
Lines =
[
new DiffLine { OldLineNumber = 1, NewLineNumber = 1, Text = "unchanged line", Type = DiffLineType.Unchanged },
new DiffLine { OldLineNumber = 2, NewLineNumber = null, Text = "removed line", Type = DiffLineType.Removed },
new DiffLine { OldLineNumber = null, NewLineNumber = 2, Text = "added line", Type = DiffLineType.Added }
],
Insertions = 1,
Deletions = 1
};
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Lines.Count.ShouldBe(3);
sut.HasChanges.ShouldBeTrue();
sut.Insertions.ShouldBe(1);
sut.Deletions.ShouldBe(1);
}
[Fact]
public void SaveCommand_SetsResultTrue_AndInvokesRequestClose()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.SaveCommand.Execute(null);
// Assert
sut.Result.ShouldBeTrue();
closeInvoked.ShouldBeTrue();
}
[Fact]
public void CancelCommand_SetsResultFalse_AndInvokesRequestClose()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.CancelCommand.Execute(null);
// Assert
sut.Result.ShouldBeFalse();
closeInvoked.ShouldBeTrue();
}
[Fact]
public void SaveCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.SaveCommand.Execute(null));
sut.Result.ShouldBeTrue();
}
[Fact]
public void CancelCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.CancelCommand.Execute(null));
sut.Result.ShouldBeFalse();
}
[Fact]
public void Constructor_ThrowsOnNullDiff()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new DiffPreviewDialogViewModel(null!));
}
[Fact]
public void Result_InitialValue_IsFalse()
{
// Arrange
var diff = CreateEmptyDiff();
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Result.ShouldBeFalse();
}
private static DiffResult CreateEmptyDiff()
{
return new DiffResult
{
HasChanges = false,
Lines = [],
Insertions = 0,
Deletions = 0
};
}
}
public class DiffLineViewModelTests
{
[Fact]
public void Constructor_UnchangedLine_SetsPropertiesCorrectly()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = 5,
NewLineNumber = 5,
Text = "unchanged content",
Type = DiffLineType.Unchanged
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("5");
sut.NewLineNumber.ShouldBe("5");
sut.Text.ShouldBe("unchanged content");
sut.Type.ShouldBe(DiffLineType.Unchanged);
sut.Background.ShouldBe("Transparent");
sut.BorderColor.ShouldBe("Transparent");
}
[Fact]
public void Constructor_AddedLine_SetsGreenStyling()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = null,
NewLineNumber = 10,
Text = "new line",
Type = DiffLineType.Added
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("");
sut.NewLineNumber.ShouldBe("10");
sut.Type.ShouldBe(DiffLineType.Added);
sut.Background.ShouldBe("#1A3DD68C");
sut.BorderColor.ShouldBe("#3DD68C");
}
[Fact]
public void Constructor_RemovedLine_SetsRedStyling()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = 7,
NewLineNumber = null,
Text = "deleted line",
Type = DiffLineType.Removed
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("7");
sut.NewLineNumber.ShouldBe("");
sut.Type.ShouldBe(DiffLineType.Removed);
sut.Background.ShouldBe("#1AFF6B6B");
sut.BorderColor.ShouldBe("#FF6B6B");
}
[Fact]
public void Constructor_NullLineNumbers_ReturnsEmptyStrings()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = null,
NewLineNumber = null,
Text = "text",
Type = DiffLineType.Unchanged
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("");
sut.NewLineNumber.ShouldBe("");
}
}
@@ -0,0 +1,181 @@
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Dialogs;
public class NewStoreDialogViewModelTests
{
[Fact]
public void StorePath_WhenEmpty_IsValidReturnsFalse()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = "",
KeyFilePath = "/path/to/key.key"
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void StorePath_WhenEmpty_ValidationErrorReturnsStorePathRequired()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = "",
KeyFilePath = "/path/to/key.key"
};
// Act & Assert
sut.ValidationError.ShouldBe(SecureStoreStrings.StorePathRequired);
}
[Fact]
public void StorePath_WhenWhitespace_IsValidReturnsFalse()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = " ",
KeyFilePath = "/path/to/key.key"
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
sut.ValidationError.ShouldBe(SecureStoreStrings.StorePathRequired);
}
[Fact]
public void KeyFilePath_WhenEmpty_IsValidReturnsFalse()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = "/path/to/store.secure",
KeyFilePath = ""
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void KeyFilePath_WhenEmpty_ValidationErrorReturnsKeyFilePathRequired()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = "/path/to/store.secure",
KeyFilePath = ""
};
// Act & Assert
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyFilePathRequired);
}
[Fact]
public void KeyFilePath_WhenProvided_IsValidReturnsTrue()
{
// Arrange
var sut = new NewStoreDialogViewModel
{
StorePath = "/path/to/store.secure",
KeyFilePath = "/path/to/key.key"
};
// Act & Assert
sut.IsValid.ShouldBeTrue();
sut.ValidationError.ShouldBeNull();
}
[Fact]
public void StorePath_WhenChanged_RaisesPropertyChanged()
{
// Arrange
var sut = new NewStoreDialogViewModel();
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(NewStoreDialogViewModel.StorePath))
propertyChangedRaised = true;
};
// Act
sut.StorePath = "/new/path";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void StorePath_WhenChanged_RaisesIsValidPropertyChanged()
{
// Arrange
var sut = new NewStoreDialogViewModel();
var isValidPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(NewStoreDialogViewModel.IsValid))
isValidPropertyChangedRaised = true;
};
// Act
sut.StorePath = "/new/path";
// Assert
isValidPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void StorePath_WhenChanged_RaisesValidationErrorPropertyChanged()
{
// Arrange
var sut = new NewStoreDialogViewModel();
var validationErrorPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(NewStoreDialogViewModel.ValidationError))
validationErrorPropertyChangedRaised = true;
};
// Act
sut.StorePath = "/new/path";
// Assert
validationErrorPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void KeyFilePath_WhenChanged_RaisesPropertyChanged()
{
// Arrange
var sut = new NewStoreDialogViewModel();
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(NewStoreDialogViewModel.KeyFilePath))
propertyChangedRaised = true;
};
// Act
sut.KeyFilePath = "/new/key/path";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Commands_AreInitialized()
{
// Arrange & Act
var sut = new NewStoreDialogViewModel();
// Assert
sut.BrowseStorePathCommand.ShouldNotBeNull();
sut.BrowseKeyFilePathCommand.ShouldNotBeNull();
sut.GenerateKeyFileCommand.ShouldNotBeNull();
}
}
@@ -0,0 +1,343 @@
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Dialogs;
public class SecretEditDialogViewModelTests
{
[Fact]
public void DefaultConstructor_SetsIsNewSecretToTrue()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel();
// Assert
sut.IsNewSecret.ShouldBeTrue();
}
[Fact]
public void DefaultConstructor_SetsKeyToEmpty()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel();
// Assert
sut.Key.ShouldBeEmpty();
}
[Fact]
public void DefaultConstructor_SetsValueToEmpty()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel();
// Assert
sut.Value.ShouldBeEmpty();
}
[Fact]
public void EditConstructor_SetsKeyFromParameter()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel("myKey", "myValue");
// Assert
sut.Key.ShouldBe("myKey");
}
[Fact]
public void EditConstructor_SetsValueFromParameter()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel("myKey", "myValue");
// Assert
sut.Value.ShouldBe("myValue");
}
[Fact]
public void EditConstructor_SetsIsNewSecretToFalse()
{
// Arrange & Act
var sut = new SecretEditDialogViewModel("myKey", "myValue");
// Assert
sut.IsNewSecret.ShouldBeFalse();
}
[Fact]
public void Key_WhenEmpty_IsValidReturnsFalse()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = ""
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void Key_WhenEmpty_ValidationErrorReturnsKeyRequired()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = ""
};
// Act & Assert
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyRequired);
}
[Fact]
public void Key_WhenWhitespace_IsValidReturnsFalse()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = " "
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyRequired);
}
[Fact]
public void Key_WhenProvided_IsValidReturnsTrue()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = "validKey"
};
// Act & Assert
sut.IsValid.ShouldBeTrue();
sut.ValidationError.ShouldBeNull();
}
[Fact]
public void IsKeyEditable_WhenIsNewSecretIsTrue_ReturnsTrue()
{
// Arrange
var sut = new SecretEditDialogViewModel();
// Act & Assert
sut.IsKeyEditable.ShouldBeTrue();
}
[Fact]
public void IsKeyEditable_WhenIsNewSecretIsFalse_ReturnsFalse()
{
// Arrange
var sut = new SecretEditDialogViewModel("existingKey", "existingValue");
// Act & Assert
sut.IsKeyEditable.ShouldBeFalse();
}
[Fact]
public void DialogTitle_WhenIsNewSecretIsTrue_ReturnsAddSecret()
{
// Arrange
var sut = new SecretEditDialogViewModel();
// Act & Assert
sut.DialogTitle.ShouldBe("Add Secret");
}
[Fact]
public void DialogTitle_WhenIsNewSecretIsFalse_ReturnsEditSecret()
{
// Arrange
var sut = new SecretEditDialogViewModel("existingKey", "existingValue");
// Act & Assert
sut.DialogTitle.ShouldBe("Edit Secret");
}
[Fact]
public void IsNewSecret_WhenChanged_RaisesIsKeyEditablePropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var isKeyEditablePropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.IsKeyEditable))
isKeyEditablePropertyChangedRaised = true;
};
// Act
sut.IsNewSecret = false;
// Assert
isKeyEditablePropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsNewSecret_WhenChanged_RaisesDialogTitlePropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var dialogTitlePropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.DialogTitle))
dialogTitlePropertyChangedRaised = true;
};
// Act
sut.IsNewSecret = false;
// Assert
dialogTitlePropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Key_WhenChanged_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.Key))
propertyChangedRaised = true;
};
// Act
sut.Key = "newKey";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Key_WhenChanged_RaisesIsValidPropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var isValidPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.IsValid))
isValidPropertyChangedRaised = true;
};
// Act
sut.Key = "newKey";
// Assert
isValidPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Key_WhenChanged_RaisesValidationErrorPropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var validationErrorPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.ValidationError))
validationErrorPropertyChangedRaised = true;
};
// Act
sut.Key = "newKey";
// Assert
validationErrorPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Value_WhenChanged_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.Value))
propertyChangedRaised = true;
};
// Act
sut.Value = "newValue";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Value_CanBeEmpty_IsValidStillReturnsTrue()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = "validKey",
Value = ""
};
// Act & Assert
sut.IsValid.ShouldBeTrue();
sut.ValidationError.ShouldBeNull();
}
[Fact]
public void Value_CanBeNull_IsValidStillReturnsTrue()
{
// Arrange
var sut = new SecretEditDialogViewModel
{
Key = "validKey",
Value = null!
};
// Act & Assert
sut.IsValid.ShouldBeTrue();
sut.ValidationError.ShouldBeNull();
}
[Fact]
public void IsNewSecret_WhenSetToSameValue_DoesNotRaisePropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel();
var propertyChangedCount = 0;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.IsNewSecret))
propertyChangedCount++;
};
// Act
sut.IsNewSecret = true; // Same value as default
// Assert
propertyChangedCount.ShouldBe(0);
}
[Fact]
public void Key_WhenSetToSameValue_DoesNotRaisePropertyChanged()
{
// Arrange
var sut = new SecretEditDialogViewModel { Key = "testKey" };
var propertyChangedCount = 0;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretEditDialogViewModel.Key))
propertyChangedCount++;
};
// Act
sut.Key = "testKey"; // Same value
// Assert
propertyChangedCount.ShouldBe(0);
}
}
@@ -0,0 +1,170 @@
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Dialogs;
public class UnlockStoreDialogViewModelTests
{
[Fact]
public void Constructor_SetsStorePathFromParameter()
{
// Arrange
var storePath = "/path/to/store.secure";
// Act
var sut = new UnlockStoreDialogViewModel(storePath);
// Assert
sut.StorePath.ShouldBe(storePath);
}
[Fact]
public void Constructor_ThrowsOnNullStorePath()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new UnlockStoreDialogViewModel(null!));
}
[Fact]
public void StorePath_IsReadOnly()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure");
// Assert - StorePath property has no setter, verified by checking it returns what was passed
sut.StorePath.ShouldBe("/path/to/store.secure");
}
[Fact]
public void KeyFilePath_WhenEmpty_IsValidReturnsFalse()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure")
{
KeyFilePath = ""
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void KeyFilePath_WhenEmpty_ValidationErrorReturnsKeyFilePathRequired()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure")
{
KeyFilePath = ""
};
// Act & Assert
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyFilePathRequired);
}
[Fact]
public void KeyFilePath_WhenWhitespace_IsValidReturnsFalse()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure")
{
KeyFilePath = " "
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyFilePathRequired);
}
[Fact]
public void KeyFilePath_WhenFileDoesNotExist_IsValidReturnsFalse()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure")
{
KeyFilePath = "/nonexistent/path/to/key.key"
};
// Act & Assert
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void KeyFilePath_WhenFileDoesNotExist_ValidationErrorReturnsKeyFileNotFound()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure")
{
KeyFilePath = "/nonexistent/path/to/key.key"
};
// Act & Assert
sut.ValidationError.ShouldBe(SecureStoreStrings.KeyFileNotFound);
}
[Fact]
public void KeyFilePath_WhenChanged_RaisesPropertyChanged()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure");
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(UnlockStoreDialogViewModel.KeyFilePath))
propertyChangedRaised = true;
};
// Act
sut.KeyFilePath = "/new/key/path";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void KeyFilePath_WhenChanged_RaisesIsValidPropertyChanged()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure");
var isValidPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(UnlockStoreDialogViewModel.IsValid))
isValidPropertyChangedRaised = true;
};
// Act
sut.KeyFilePath = "/new/key/path";
// Assert
isValidPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void KeyFilePath_WhenChanged_RaisesValidationErrorPropertyChanged()
{
// Arrange
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure");
var validationErrorPropertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(UnlockStoreDialogViewModel.ValidationError))
validationErrorPropertyChangedRaised = true;
};
// Act
sut.KeyFilePath = "/new/key/path";
// Assert
validationErrorPropertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void BrowseKeyFilePathCommand_IsInitialized()
{
// Arrange & Act
var sut = new UnlockStoreDialogViewModel("/path/to/store.secure");
// Assert
sut.BrowseKeyFilePathCommand.ShouldNotBeNull();
}
}
@@ -0,0 +1,193 @@
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Dialogs;
public class ValidationResultsDialogViewModelTests
{
[Fact]
public void Constructor_WithEmptyResults_HasNoItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(0);
sut.ErrorCount.ShouldBe(0);
sut.WarningCount.ShouldBe(0);
sut.IsValid.ShouldBeTrue();
}
[Fact]
public void Constructor_WithErrors_PopulatesItemsCorrectly()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("Missing connection string");
appSettingsResult.AddError("Invalid timeout value");
var pipelinesResult = new ValidationResult();
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(2);
sut.ErrorCount.ShouldBe(2);
sut.WarningCount.ShouldBe(0);
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void Constructor_WithWarnings_PopulatesItemsCorrectly()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddWarning("Deprecated setting used");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddWarning("Pipeline has no transformers");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(2);
sut.ErrorCount.ShouldBe(0);
sut.WarningCount.ShouldBe(2);
sut.IsValid.ShouldBeFalse(); // Both errors and warnings make it invalid
}
[Fact]
public void Constructor_WithMixedResults_PopulatesAllItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("Error in appsettings");
appSettingsResult.AddWarning("Warning in appsettings");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddError("Error in pipelines");
pipelinesResult.AddWarning("Warning in pipelines");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(4);
sut.ErrorCount.ShouldBe(2);
sut.WarningCount.ShouldBe(2);
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void Constructor_SetsCorrectSourceOnItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("App error");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddError("Pipeline error");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.ShouldContain(i => i.Source == "appsettings.json" && i.Message == "App error");
sut.Items.ShouldContain(i => i.Source == "pipelines.json" && i.Message == "Pipeline error");
}
[Fact]
public void CloseCommand_InvokesRequestClose()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.CloseCommand.Execute(null);
// Assert
closeInvoked.ShouldBeTrue();
}
[Fact]
public void CloseCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.CloseCommand.Execute(null));
}
[Fact]
public void Constructor_ThrowsOnNullAppSettingsResult()
{
// Arrange
var pipelinesResult = new ValidationResult();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ValidationResultsDialogViewModel(null!, pipelinesResult));
}
[Fact]
public void Constructor_ThrowsOnNullPipelinesResult()
{
// Arrange
var appSettingsResult = new ValidationResult();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ValidationResultsDialogViewModel(appSettingsResult, null!));
}
}
public class ValidationItemViewModelTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Act
var sut = new ValidationItemViewModel("Test message", "test.json", ValidationItemType.Error);
// Assert
sut.Message.ShouldBe("Test message");
sut.Source.ShouldBe("test.json");
sut.Type.ShouldBe(ValidationItemType.Error);
}
[Fact]
public void Constructor_ErrorType_SetsErrorStyling()
{
// Act
var sut = new ValidationItemViewModel("Error", "test.json", ValidationItemType.Error);
// Assert
sut.Icon.ShouldBe("\u2717"); // X mark
sut.IconColor.ShouldBe("#FF6B6B");
sut.Background.ShouldBe("#1AFF6B6B");
sut.BorderColor.ShouldBe("#FF6B6B");
}
[Fact]
public void Constructor_WarningType_SetsWarningStyling()
{
// Act
var sut = new ValidationItemViewModel("Warning", "test.json", ValidationItemType.Warning);
// Assert
sut.Icon.ShouldBe("\u26A0"); // Warning sign
sut.IconColor.ShouldBe("#FFB84D");
sut.Background.ShouldBe("#1AFFB84D");
sut.BorderColor.ShouldBe("#FFB84D");
}
}
@@ -0,0 +1,91 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class AuthFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new AuthSection
{
CookieName = ".TestApp.Auth",
CookieExpirationMinutes = 120
};
// Act
var sut = new AuthFormViewModel(model, () => { });
// Assert
sut.CookieName.ShouldBe(".TestApp.Auth");
sut.CookieExpirationMinutes.ShouldBe(120);
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new AuthSection();
var changedInvoked = false;
var sut = new AuthFormViewModel(model, () => changedInvoked = true);
// Act
sut.CookieName = ".Custom.Cookie";
// Assert
model.CookieName.ShouldBe(".Custom.Cookie");
changedInvoked.ShouldBeTrue();
}
[Fact]
public void CookieExpirationMinutes_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new AuthSection { CookieExpirationMinutes = 480 };
var changedInvoked = false;
var sut = new AuthFormViewModel(model, () => changedInvoked = true);
// Act
sut.CookieExpirationMinutes = 60;
// Assert
model.CookieExpirationMinutes.ShouldBe(60);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void PropertyChange_RaisesPropertyChanged()
{
// Arrange
var model = new AuthSection();
var sut = new AuthFormViewModel(model, () => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(AuthFormViewModel.CookieName))
propertyChangedRaised = true;
};
// Act
sut.CookieName = ".New.Cookie";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new AuthFormViewModel(null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new AuthFormViewModel(new AuthSection(), null!));
}
}
@@ -0,0 +1,219 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class ConnectionStringEntryViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
SqlServerPort = 1434,
Database = "TestDb",
UserId = "testuser",
Password = "testpass",
Encrypt = "Strict",
TrustServerCertificate = true,
ConnectionTimeout = 45,
ApplicationName = "TestApp"
};
// Act
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.Name.ShouldBe("TestConnection");
sut.Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Server.ShouldBe("localhost");
sut.SqlServerPort.ShouldBe(1434);
sut.Database.ShouldBe("TestDb");
sut.UserId.ShouldBe("testuser");
sut.Password.ShouldBe("testpass");
sut.Encrypt.ShouldBe("Strict");
sut.TrustServerCertificate.ShouldBeTrue();
sut.ConnectionTimeout.ShouldBe(45);
sut.ApplicationName.ShouldBe("TestApp");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ConnectionStringEntryViewModel(null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = new ConnectionStringEntry();
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ConnectionStringEntryViewModel(model, null!));
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new ConnectionStringEntry();
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Act
sut.Name = "UpdatedName";
sut.Server = "newserver";
sut.Database = "newdb";
// Assert
model.Name.ShouldBe("UpdatedName");
model.Server.ShouldBe("newserver");
model.Database.ShouldBe("newdb");
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new ConnectionStringEntry();
var changedInvoked = false;
var sut = new ConnectionStringEntryViewModel(model, () => changedInvoked = true);
// Act
sut.Name = "NewName";
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void TogglePasswordVisibility_TogglesIsPasswordVisible()
{
// Arrange
var model = new ConnectionStringEntry();
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert initial state
sut.IsPasswordVisible.ShouldBeFalse();
// Act - first toggle
sut.TogglePasswordVisibilityCommand.Execute(null);
// Assert
sut.IsPasswordVisible.ShouldBeTrue();
// Act - second toggle
sut.TogglePasswordVisibilityCommand.Execute(null);
// Assert
sut.IsPasswordVisible.ShouldBeFalse();
}
[Fact]
public void ProviderDisplay_ReturnsCorrectString()
{
// Arrange
var model = new ConnectionStringEntry { Provider = ConnectionProvider.SqlServer };
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ProviderDisplay.ShouldBe("SqlServer");
// Act
sut.Provider = ConnectionProvider.Oracle;
// Assert
sut.ProviderDisplay.ShouldBe("Oracle");
// Act
sut.Provider = ConnectionProvider.Generic;
// Assert
sut.ProviderDisplay.ShouldBe("Generic");
}
[Fact]
public void ServerDisplay_ReturnsServerForSqlServer()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.SqlServer,
Server = "sql-server-host"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("sql-server-host");
}
[Fact]
public void ServerDisplay_ReturnsHostForOracle()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.Oracle,
Host = "oracle-host"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("oracle-host");
}
[Fact]
public void ServerDisplay_ReturnsDashForGeneric()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.Generic,
RawConnectionString = "some connection string"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("-");
}
[Fact]
public void SqlServerPort_PropertyChange_UpdatesModel()
{
// Arrange
var model = new ConnectionStringEntry();
var changedInvoked = false;
var sut = new ConnectionStringEntryViewModel(model, () => changedInvoked = true);
// Act
sut.SqlServerPort = 1434;
// Assert
model.SqlServerPort.ShouldBe(1434);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void SqlServerPort_PropertyChange_UpdatesGeneratedConnectionString()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Act
sut.SqlServerPort = 1434;
// Assert
sut.GeneratedConnectionString.ShouldContain("Server=localhost,1434");
}
}
@@ -0,0 +1,664 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Ui.Services;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class ConnectionStringsFormViewModelTests
{
private readonly ISecureStoreManager _secureStoreManager;
private readonly IDialogService _dialogService;
private readonly IConnectionTestService _connectionTestService;
public ConnectionStringsFormViewModelTests()
{
_secureStoreManager = Substitute.For<ISecureStoreManager>();
_dialogService = Substitute.For<IDialogService>();
_connectionTestService = Substitute.For<IConnectionTestService>();
// Setup default behavior - SecureStore is not open by default in tests
_secureStoreManager.IsStoreOpen.Returns(false);
}
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "Connection1",
Provider = ConnectionProvider.SqlServer,
Server = "server1"
},
new ConnectionStringEntry
{
Name = "Connection2",
Provider = ConnectionProvider.Oracle,
Host = "oracle-host"
}
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(2);
sut.Connections[0].Name.ShouldBe("Connection1");
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("server1");
sut.Connections[1].Name.ShouldBe("Connection2");
sut.Connections[1].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[1].Host.ShouldBe("oracle-host");
}
[Fact]
public void Constructor_LoadsAndParsesSqlServerConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "LotFinder" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("LotFinder")
.Returns("Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=pass;TrustServerCertificate=true");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert - port stays embedded in server name, not parsed into SqlServerPort
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("localhost,1434");
sut.Connections[0].SqlServerPort.ShouldBeNull();
sut.Connections[0].Database.ShouldBe("ScopingTool");
sut.Connections[0].UserId.ShouldBe("scopingapp");
sut.Connections[0].Password.ShouldBe("pass");
sut.Connections[0].TrustServerCertificate.ShouldBeTrue();
}
[Fact]
public void Constructor_LoadsAndParsesOracleConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "CMS" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("CMS")
.Returns("HOST=ha-iman;Service Name=imanprd;Fetch Array Size=1280000;Port=1522;User ID=app_teamcenter;Password=pass;");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[0].Host.ShouldBe("ha-iman");
sut.Connections[0].ServiceName.ShouldBe("imanprd");
sut.Connections[0].Port.ShouldBe(1522);
sut.Connections[0].UserId.ShouldBe("app_teamcenter");
sut.Connections[0].Password.ShouldBe("pass");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(null!, _secureStoreManager, () => { }, _dialogService, _connectionTestService));
}
[Fact]
public void Constructor_ThrowsOnNullSecureStoreManager()
{
// Arrange
var model = new ConnectionStringsSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(model, null!, () => { }, _dialogService, _connectionTestService));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = new ConnectionStringsSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(model, _secureStoreManager, null!, _dialogService, _connectionTestService));
}
[Fact]
public void Constructor_ThrowsOnNullConnectionTestService()
{
// Arrange
var model = new ConnectionStringsSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, null!));
}
[Fact]
public void AddConnection_CreatesNewEntryAndSelectsIt()
{
// Arrange
var model = new ConnectionStringsSection();
var changedInvoked = false;
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => changedInvoked = true, _dialogService, _connectionTestService);
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("NewConnection");
sut.SelectedConnection.ShouldBe(sut.Connections[0]);
model.Entries.Count.ShouldBe(1);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void HasSelection_IsFalseWhenNothingSelected()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert - no selection by default
sut.SelectedConnection.ShouldBeNull();
sut.HasSelection.ShouldBeFalse();
}
[Fact]
public void HasSelection_IsTrueWhenConnectionSelected()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Act
sut.SelectedConnection = sut.Connections[0];
// Assert
sut.HasSelection.ShouldBeTrue();
}
[Fact]
public void ConnectionCount_ReflectsCollectionSize()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" },
new ConnectionStringEntry { Name = "Conn2" },
new ConnectionStringEntry { Name = "Conn3" }
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.ConnectionCount.ShouldBe(3);
// Act - add another
sut.AddConnectionCommand.Execute(null);
// Assert
sut.ConnectionCount.ShouldBe(4);
}
[Fact]
public void Constructor_InitializesConnectionsFromModel()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb",
UserId = "sa",
Password = "secret"
}
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("TestConnection");
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("localhost");
sut.Connections[0].Database.ShouldBe("TestDb");
}
[Fact]
public void AddConnectionCommand_AddsNewConnection()
{
// Arrange
var model = new ConnectionStringsSection();
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
var initialCount = sut.Connections.Count;
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
sut.Connections.Count.ShouldBe(initialCount + 1);
sut.Connections.Last().Name.ShouldBe("NewConnection");
sut.Connections.Last().Provider.ShouldBe(ConnectionProvider.Generic);
}
[Fact]
public async Task DeleteConnectionCommand_WhenConfirmed_RemovesConnection()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToDelete" }
}
};
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
sut.Connections.Count.ShouldBe(0);
model.Entries.Count.ShouldBe(0);
}
[Fact]
public async Task DeleteConnectionCommand_WhenCancelled_KeepsConnection()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToKeep" }
}
};
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(false);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("ToKeep");
}
[Fact]
public async Task ValidateConnectionCommand_WithEmptyConnectionString_ShowsError()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "EmptyConnection",
Provider = ConnectionProvider.Generic,
RawConnectionString = ""
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.ValidateConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Validation Failed",
Arg.Is<string>(s => s.Contains("empty connection string")));
}
[Fact]
public async Task ValidateConnectionCommand_WithValidConnectionString_ShowsSuccess()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "ValidConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.ValidateConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Validation Passed",
Arg.Is<string>(s => s.Contains("valid connection string")));
}
[Fact]
public async Task TestConnectionCommand_WhenSuccessful_ShowsSuccessMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(new ConnectionTestResult { Success = true, Duration = TimeSpan.FromMilliseconds(50) });
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(150);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Connection Successful",
Arg.Is<string>(s => s.Contains("Successfully connected")));
}
[Fact]
public async Task TestConnectionCommand_WhenFailed_ShowsErrorMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "FailConn",
Provider = ConnectionProvider.SqlServer,
Server = "badserver",
Database = "TestDb"
}
}
};
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(new ConnectionTestResult { Success = false, Message = "Connection refused" });
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(150);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Connection Failed",
Arg.Is<string>(s => s.Contains("Failed to connect") && s.Contains("Connection refused")));
}
[Fact]
public async Task TestConnectionCommand_SetsIsTesting_DuringExecution()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
var taskCompletionSource = new TaskCompletionSource<ConnectionTestResult>();
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(taskCompletionSource.Task);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act - Start the command
sut.TestConnectionCommand.Execute(null);
await Task.Delay(50);
// Assert - IsTesting should be true during execution
sut.IsTesting.ShouldBeTrue();
// Complete the task
taskCompletionSource.SetResult(new ConnectionTestResult { Success = true });
await Task.Delay(100);
// Assert - IsTesting should be false after completion
sut.IsTesting.ShouldBeFalse();
}
[Fact]
public void SelectedConnection_WhenChanged_RaisesHasSelectionPropertyChanged()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
var hasSelectionChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ConnectionStringsFormViewModel.HasSelection))
hasSelectionChangedRaised = true;
};
// Act
sut.SelectedConnection = sut.Connections[0];
// Assert
hasSelectionChangedRaised.ShouldBeTrue();
}
[Fact]
public void OnEntryChanged_SavesValueToSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
var onChangedCalled = false;
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => onChangedCalled = true, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act - Change a property on the selected connection
sut.SelectedConnection.Database = "NewDatabase";
// Assert
onChangedCalled.ShouldBeTrue();
_secureStoreManager.Received().SetSecret("TestConn", Arg.Any<string>());
}
[Fact]
public async Task DeleteConnectionCommand_RemovesFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToDelete" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
_secureStoreManager.Received().RemoveSecret("ToDelete");
}
[Fact]
public void AddConnectionCommand_CreatesSecureStoreEntry()
{
// Arrange
var model = new ConnectionStringsSection();
_secureStoreManager.IsStoreOpen.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
_secureStoreManager.Received().SetSecret("NewConnection", string.Empty);
}
[Fact]
public async Task TestConnectionCommand_WithEmptyConnectionString_ShowsMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "EmptyConn",
Provider = ConnectionProvider.Generic,
RawConnectionString = ""
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Test Connection",
Arg.Is<string>(s => s.Contains("connection string is empty")));
await _connectionTestService.DidNotReceive().TestConnectionAsync(
Arg.Any<string>(),
Arg.Any<ConnectionProvider>(),
Arg.Any<CancellationToken>());
}
[Fact]
public void AvailableProviders_ContainsAllProviders()
{
// Assert
ConnectionStringsFormViewModel.AvailableProviders.Count.ShouldBe(
Enum.GetValues<ConnectionProvider>().Length);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.SqlServer);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.Oracle);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.Generic);
}
[Fact]
public void EncryptOptions_ContainsExpectedValues()
{
// Assert
ConnectionStringsFormViewModel.EncryptOptions.Count.ShouldBe(3);
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("True");
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("False");
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("Strict");
}
}
@@ -0,0 +1,43 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class DataAccessFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new DataAccessSection
{
DefaultTimeoutSeconds = 60,
ProductionSchema = "dbo",
EnableDetailedLogging = true
};
// Act
var sut = new DataAccessFormViewModel(model, () => { });
// Assert
sut.DefaultTimeoutSeconds.ShouldBe(60);
sut.ProductionSchema.ShouldBe("dbo");
sut.EnableDetailedLogging.ShouldBeTrue();
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new DataAccessSection();
var changedInvoked = false;
var sut = new DataAccessFormViewModel(model, () => changedInvoked = true);
// Act
sut.ArchiveSchema = "hist";
// Assert
model.ArchiveSchema.ShouldBe("hist");
changedInvoked.ShouldBeTrue();
}
}
@@ -0,0 +1,76 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class DataSyncFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new DataSyncSection
{
Enabled = true,
MaxDegreeOfParallelism = 8,
BatchSize = 25000
};
// Act
var sut = new DataSyncFormViewModel(model, () => { });
// Assert
sut.Enabled.ShouldBeTrue();
sut.MaxDegreeOfParallelism.ShouldBe(8);
sut.BatchSize.ShouldBe(25000);
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new DataSyncSection { MaxDegreeOfParallelism = 4 };
var sut = new DataSyncFormViewModel(model, () => { });
// Act
sut.MaxDegreeOfParallelism = 16;
// Assert
model.MaxDegreeOfParallelism.ShouldBe(16);
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new DataSyncSection();
var changedInvoked = false;
var sut = new DataSyncFormViewModel(model, () => changedInvoked = true);
// Act
sut.BatchSize = 10000;
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void PropertyChange_RaisesPropertyChanged()
{
// Arrange
var model = new DataSyncSection();
var sut = new DataSyncFormViewModel(model, () => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(DataSyncFormViewModel.LookbackMultiplier))
propertyChangedRaised = true;
};
// Act
sut.LookbackMultiplier = 2.5;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
}
@@ -0,0 +1,149 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class ExcelExportFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ExcelExportSection
{
MaxRowsPerSheet = 500000,
DefaultDateFormat = "MM/dd/yyyy",
DebugWriteToFile = true,
DebugOutputDirectory = "/tmp/debug",
TimezoneId = "America/Los_Angeles"
};
// Act
var sut = new ExcelExportFormViewModel(model, () => { });
// Assert
sut.MaxRowsPerSheet.ShouldBe(500000);
sut.DefaultDateFormat.ShouldBe("MM/dd/yyyy");
sut.DebugWriteToFile.ShouldBeTrue();
sut.DebugOutputDirectory.ShouldBe("/tmp/debug");
sut.SelectedTimezone.ShouldBe("America/Los_Angeles");
}
[Fact]
public void Constructor_UsesModelTimezone()
{
// Arrange - model defaults to "America/Chicago"
var model = new ExcelExportSection();
// Act
var sut = new ExcelExportFormViewModel(model, () => { });
// Assert
sut.SelectedTimezone.ShouldBe("America/Chicago");
}
[Fact]
public void AvailableTimezones_ContainsSystemTimezones()
{
// Act & Assert
ExcelExportFormViewModel.AvailableTimezones.ShouldNotBeEmpty();
// Check for common IANA timezones
ExcelExportFormViewModel.AvailableTimezones.ShouldContain("America/Chicago");
ExcelExportFormViewModel.AvailableTimezones.ShouldContain("UTC");
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new ExcelExportSection { MaxRowsPerSheet = 1000000 };
var sut = new ExcelExportFormViewModel(model, () => { });
// Act
sut.MaxRowsPerSheet = 750000;
// Assert
model.MaxRowsPerSheet.ShouldBe(750000);
}
[Fact]
public void SelectedTimezone_UpdatesModelTimezoneId()
{
// Arrange - model defaults to "America/Chicago"
var model = new ExcelExportSection();
var sut = new ExcelExportFormViewModel(model, () => { });
// Act - change to a different timezone
sut.SelectedTimezone = "America/New_York";
// Assert
model.TimezoneId.ShouldBe("America/New_York");
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new ExcelExportSection(); // Default TimezoneId is "America/Chicago"
var changedInvoked = false;
var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true);
// Act - change to a different timezone than the default
sut.SelectedTimezone = "America/Denver";
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void PropertyChange_RaisesPropertyChanged()
{
// Arrange
var model = new ExcelExportSection();
var sut = new ExcelExportFormViewModel(model, () => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ExcelExportFormViewModel.DefaultDateFormat))
propertyChangedRaised = true;
};
// Act
sut.DefaultDateFormat = "dd-MMM-yyyy";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void SameValueAssignment_DoesNotInvokeOnChanged()
{
// Arrange
var model = new ExcelExportSection { DebugWriteToFile = true };
var changedInvoked = false;
var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true);
// Act
sut.DebugWriteToFile = true; // Same value
// Assert
changedInvoked.ShouldBeFalse();
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ExcelExportFormViewModel(null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = new ExcelExportSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ExcelExportFormViewModel(model, null!));
}
}
@@ -0,0 +1,60 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class LdapFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new LdapSection
{
ServerUrls = ["ldap://server1.local", "ldap://server2.local"],
GroupDn = "CN=Admins,DC=corp",
SearchBase = "DC=corp,DC=local",
UseFakeAuth = true
};
// Act
var sut = new LdapFormViewModel(model, () => { });
// Assert
sut.ServerUrlsText.ShouldBe("ldap://server1.local\nldap://server2.local");
sut.GroupDn.ShouldBe("CN=Admins,DC=corp");
sut.SearchBase.ShouldBe("DC=corp,DC=local");
sut.UseFakeAuth.ShouldBeTrue();
}
[Fact]
public void ServerUrlsText_SplitsIntoArray()
{
// Arrange
var model = new LdapSection();
var sut = new LdapFormViewModel(model, () => { });
// Act
sut.ServerUrlsText = "ldap://a.local\nldap://b.local\nldap://c.local";
// Assert
model.ServerUrls.Length.ShouldBe(3);
model.ServerUrls[0].ShouldBe("ldap://a.local");
model.ServerUrls[2].ShouldBe("ldap://c.local");
}
[Fact]
public void AdminBypassUsersText_SplitsIntoArray()
{
// Arrange
var model = new LdapSection();
var sut = new LdapFormViewModel(model, () => { });
// Act
sut.AdminBypassUsersText = "admin\nservice_account";
// Assert
model.AdminBypassUsers.Length.ShouldBe(2);
model.AdminBypassUsers[0].ShouldBe("admin");
}
}
@@ -0,0 +1,630 @@
using JdeScoping.ConfigManager.Ui.Services;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
using JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class PipelineEditorViewModelTests
{
private readonly IDialogService _dialogService;
public PipelineEditorViewModelTests()
{
_dialogService = Substitute.For<IDialogService>();
}
[Fact]
public void Constructor_InitializesPropertiesCorrectly()
{
// Arrange
var model = CreateDefaultModel();
var connections = new List<string> { "jde", "cms", "lotfinder" };
// Act
var sut = new PipelineEditorViewModel("TestPipeline", model, connections, _dialogService, () => { });
// Assert
sut.Name.ShouldBe("TestPipeline");
sut.AvailableConnections.ShouldBe(connections);
sut.PreScripts.ShouldNotBeNull();
sut.Transformers.ShouldNotBeNull();
sut.PostScripts.ShouldNotBeNull();
sut.Source.ShouldNotBeNull();
sut.Destination.ShouldNotBeNull();
}
[Fact]
public void Constructor_BuildsSourceAndDestinationFromModel()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder_Curr" }
};
var connections = new List<string> { "jde" };
// Act
var sut = new PipelineEditorViewModel("TestPipeline", model, connections, _dialogService, () => { });
// Assert
sut.Source.Connection.ShouldBe("jde");
sut.Source.Query.ShouldBe("SELECT * FROM WO");
sut.Destination.Table.ShouldBe("WorkOrder_Curr");
}
[Fact]
public void IsEnabled_Setter_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = CreateDefaultModel();
model.IsEnabled = false;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.IsEnabled = true;
// Assert
sut.IsEnabled.ShouldBeTrue();
model.IsEnabled.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void IsManualOnly_Setter_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = CreateDefaultModel();
model.IsManualOnly = false;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.IsManualOnly = true;
// Assert
sut.IsManualOnly.ShouldBeTrue();
model.IsManualOnly.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MassSyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.MassSyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.MassSyncEnabled = true;
// Assert
sut.MassSyncEnabled.ShouldBeTrue();
sut.MassSyncIntervalMinutes.ShouldBe(10080); // 1 week default
model.MassSyncIntervalMinutes.ShouldBe(10080);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MassSyncEnabled_ToggleOff_ClearsInterval()
{
// Arrange
var model = CreateDefaultModel();
model.MassSyncIntervalMinutes = 10080;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.MassSyncEnabled = false;
// Assert
sut.MassSyncEnabled.ShouldBeFalse();
model.MassSyncIntervalMinutes.ShouldBeNull();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void DailySyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.DailySyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.DailySyncEnabled = true;
// Assert
sut.DailySyncEnabled.ShouldBeTrue();
sut.DailySyncIntervalMinutes.ShouldBe(1440); // 1 day default
model.DailySyncIntervalMinutes.ShouldBe(1440);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void HourlySyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.HourlySyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.HourlySyncEnabled = true;
// Assert
sut.HourlySyncEnabled.ShouldBeTrue();
sut.HourlySyncIntervalMinutes.ShouldBe(60); // 1 hour default
model.HourlySyncIntervalMinutes.ShouldBe(60);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void SelectedStep_Setter_DeselectsPreviousAndSelectsNew()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
var source = sut.Source;
var destination = sut.Destination;
// Act - Select source
sut.SelectedStep = source;
// Assert
source.IsSelected.ShouldBeTrue();
sut.SelectedStep.ShouldBe(source);
// Act - Select destination (should deselect source)
sut.SelectedStep = destination;
// Assert
source.IsSelected.ShouldBeFalse();
destination.IsSelected.ShouldBeTrue();
sut.SelectedStep.ShouldBe(destination);
}
[Fact]
public void CanDeleteSelectedStep_ReturnsFalse_ForSourceStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Source;
// Assert
sut.CanDeleteSelectedStep.ShouldBeFalse();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsFalse_ForDestinationStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Destination;
// Assert
sut.CanDeleteSelectedStep.ShouldBeFalse();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForTransformerStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a transformer
sut.AddTransformerCommand.Execute(null);
var transformer = sut.Transformers[0];
// Act
sut.SelectedStep = transformer;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForPreScriptStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a pre-script
sut.AddPreScriptCommand.Execute(null);
var preScript = sut.PreScripts[0];
// Act
sut.SelectedStep = preScript;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForPostScriptStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a post-script
sut.AddPostScriptCommand.Execute(null);
var postScript = sut.PostScripts[0];
// Act
sut.SelectedStep = postScript;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void AddPreScriptCommand_AddsPreScriptAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddPreScriptCommand.Execute(null);
// Assert
sut.PreScripts.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.PreScripts[0]);
sut.PreScripts[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void AddTransformerCommand_AddsTransformerAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddTransformerCommand.Execute(null);
// Assert
sut.Transformers.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.Transformers[0]);
sut.Transformers[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void AddPostScriptCommand_AddsPostScriptAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddPostScriptCommand.Execute(null);
// Assert
sut.PostScripts.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.PostScripts[0]);
sut.PostScripts[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void RemoveStepCommand_RemovesSelectedStep()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Add a transformer first
sut.AddTransformerCommand.Execute(null);
var transformer = sut.Transformers[0];
changedInvoked = false;
// Act
sut.RemoveStepCommand.Execute(transformer);
// Assert
sut.Transformers.Count.ShouldBe(0);
sut.SelectedStep.ShouldBeNull();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MoveStepUpCommand_MovesStepUpInCollection()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Move second up
sut.MoveStepUpCommand.Execute(secondTransformer);
// Assert
sut.Transformers[0].ShouldBe(secondTransformer);
sut.Transformers[1].ShouldBe(firstTransformer);
}
[Fact]
public void MoveStepDownCommand_MovesStepDownInCollection()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Move first down
sut.MoveStepDownCommand.Execute(firstTransformer);
// Assert
sut.Transformers[0].ShouldBe(secondTransformer);
sut.Transformers[1].ShouldBe(firstTransformer);
}
[Fact]
public void MoveStepUpCommand_DoesNotMove_WhenAtTop()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Try to move first up (should do nothing)
sut.MoveStepUpCommand.Execute(firstTransformer);
// Assert
sut.Transformers[0].ShouldBe(firstTransformer);
sut.Transformers[1].ShouldBe(secondTransformer);
}
[Fact]
public void MoveStepDownCommand_DoesNotMove_WhenAtBottom()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Try to move second down (should do nothing)
sut.MoveStepDownCommand.Execute(secondTransformer);
// Assert
sut.Transformers[0].ShouldBe(firstTransformer);
sut.Transformers[1].ShouldBe(secondTransformer);
}
[Fact]
public void Constructor_ThrowsOnNullName()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel(null!, model, [], _dialogService, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", null!, [], _dialogService, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullDialogService()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", model, [], null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", model, [], _dialogService, null!));
}
[Fact]
public void Constructor_LoadsExistingTransformers()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
Transforms =
[
new TransformElement { TransformType = "ColumnDrop" },
new TransformElement { TransformType = "ColumnRename" }
]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.Transformers.Count.ShouldBe(2);
sut.Transformers[0].ShouldBeOfType<ColumnDropTransformerViewModel>();
sut.Transformers[1].ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void Constructor_LoadsExistingPreScripts()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PreScripts = [new ScriptElement { Connection = "lotfinder", Script = "TRUNCATE TABLE Test" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.PreScripts.Count.ShouldBe(1);
sut.PreScripts[0].Script.ShouldBe("TRUNCATE TABLE Test");
}
[Fact]
public void Constructor_LoadsExistingPostScripts()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PostScripts = [new ScriptElement { Connection = "lotfinder", Script = "UPDATE Stats SET LastRun = GETDATE()" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.PostScripts.Count.ShouldBe(1);
sut.PostScripts[0].Script.ShouldBe("UPDATE Stats SET LastRun = GETDATE()");
}
[Fact]
public void AllSteps_ReturnsAllStepsInOrder()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PreScripts = [new ScriptElement { Script = "pre" }],
Transforms = [new TransformElement { TransformType = "ColumnDrop" }],
PostScripts = [new ScriptElement { Script = "post" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
var allSteps = sut.AllSteps.ToList();
// Assert
allSteps.Count.ShouldBe(5);
allSteps[0].ShouldBeOfType<PreScriptStepViewModel>();
allSteps[1].ShouldBeOfType<SourceStepViewModel>();
allSteps[2].ShouldBeOfType<ColumnDropTransformerViewModel>();
allSteps[3].ShouldBeOfType<DestinationStepViewModel>();
allSteps[4].ShouldBeOfType<PostScriptStepViewModel>();
}
[Fact]
public void SelectedStepEditor_UpdatesWhenSelectedStepChanges()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Source;
// Assert
sut.SelectedStepEditor.ShouldBe(sut.Source);
}
[Fact]
public void AddTransformerOfType_AddsSpecificTransformerType()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.AddTransformerOfType("JdeDate");
// Assert
sut.Transformers.Count.ShouldBe(1);
sut.Transformers[0].ShouldBeOfType<JdeDateTransformerViewModel>();
sut.SelectedStep.ShouldBe(sut.Transformers[0]);
}
[Fact]
public void SelectedTransformerType_SetterAndGetter_Work()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedTransformerType = "ColumnRename";
// Assert
sut.SelectedTransformerType.ShouldBe("ColumnRename");
}
[Fact]
public void AvailableTransformerTypes_ReturnsExpectedTypes()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.AvailableTransformerTypes.ShouldContain("ColumnDrop");
sut.AvailableTransformerTypes.ShouldContain("ColumnRename");
sut.AvailableTransformerTypes.ShouldContain("JdeDate");
sut.AvailableTransformerTypes.ShouldContain("Regex");
}
private static EtlPipelineConfig CreateDefaultModel()
{
return new EtlPipelineConfig
{
Source = new SourceElement { Connection = string.Empty, Query = string.Empty },
Destination = new DestinationElement { Table = string.Empty }
};
}
}
@@ -0,0 +1,108 @@
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class SearchFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new SearchSection
{
MaxResultRows = 50000,
TimeoutSeconds = 600,
MaxConcurrentSearches = 10
};
// Act
var sut = new SearchFormViewModel(model, () => { });
// Assert
sut.MaxResultRows.ShouldBe(50000);
sut.TimeoutSeconds.ShouldBe(600);
sut.MaxConcurrentSearches.ShouldBe(10);
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new SearchSection { MaxResultRows = 100000 };
var sut = new SearchFormViewModel(model, () => { });
// Act
sut.MaxResultRows = 25000;
// Assert
model.MaxResultRows.ShouldBe(25000);
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new SearchSection();
var changedInvoked = false;
var sut = new SearchFormViewModel(model, () => changedInvoked = true);
// Act
sut.TimeoutSeconds = 120;
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void PropertyChange_RaisesPropertyChanged()
{
// Arrange
var model = new SearchSection();
var sut = new SearchFormViewModel(model, () => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SearchFormViewModel.MaxConcurrentSearches))
propertyChangedRaised = true;
};
// Act
sut.MaxConcurrentSearches = 8;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new SearchFormViewModel(null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullCallback()
{
// Arrange
var model = new SearchSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() => new SearchFormViewModel(model, null!));
}
[Fact]
public void PropertyChange_DoesNotInvokeOnChangedWhenValueUnchanged()
{
// Arrange
var model = new SearchSection { MaxResultRows = 50000 };
var changedCount = 0;
var sut = new SearchFormViewModel(model, () => changedCount++);
// Act - set to same value
sut.MaxResultRows = 50000;
// Assert
changedCount.ShouldBe(0);
}
}
@@ -0,0 +1,412 @@
using JdeScoping.ConfigManager.Ui.Services;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.Forms;
public class SecretFormViewModelTests
{
private readonly IClipboardService _clipboardService;
public SecretFormViewModelTests()
{
_clipboardService = Substitute.For<IClipboardService>();
}
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Arrange & Act
var sut = new SecretFormViewModel(
"API_KEY",
"secret-value-123",
_clipboardService,
_ => { },
() => { });
// Assert
sut.Key.ShouldBe("API_KEY");
sut.Value.ShouldBe("secret-value-123");
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public void Constructor_WithNullValue_SetsEmptyString()
{
// Arrange & Act
var sut = new SecretFormViewModel(
"API_KEY",
null!,
_clipboardService,
_ => { },
() => { });
// Assert
sut.Value.ShouldBe(string.Empty);
}
[Fact]
public void Value_Setter_InvokesCallback()
{
// Arrange
string? changedValue = null;
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
v => changedValue = v,
() => { });
// Act
sut.Value = "new-value";
// Assert
changedValue.ShouldBe("new-value");
}
[Fact]
public void Value_Setter_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
_ => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.Value))
propertyChangedRaised = true;
};
// Act
sut.Value = "new-value";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Value_Setter_RaisesDisplayValuePropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
_ => { },
() => { });
var displayValueChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.DisplayValue))
displayValueChangedRaised = true;
};
// Act
sut.Value = "new-value";
// Assert
displayValueChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsValueVisible_Toggle_Works()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert - Initial state
sut.IsValueVisible.ShouldBeFalse();
// Act - Toggle on
sut.IsValueVisible = true;
sut.IsValueVisible.ShouldBeTrue();
// Act - Toggle off
sut.IsValueVisible = false;
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public void IsValueVisible_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.IsValueVisible))
propertyChangedRaised = true;
};
// Act
sut.IsValueVisible = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsValueVisible_RaisesDisplayValueAndVisibilityButtonTextPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
var displayValueChangedRaised = false;
var visibilityButtonTextChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.DisplayValue))
displayValueChangedRaised = true;
if (e.PropertyName == nameof(SecretFormViewModel.VisibilityButtonText))
visibilityButtonTextChangedRaised = true;
};
// Act
sut.IsValueVisible = true;
// Assert
displayValueChangedRaised.ShouldBeTrue();
visibilityButtonTextChangedRaised.ShouldBeTrue();
}
[Fact]
public void DisplayValue_ShowsMaskedValue_WhenNotVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret123",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.IsValueVisible.ShouldBeFalse();
sut.DisplayValue.ShouldBe("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"); // 9 bullet points
}
[Fact]
public void DisplayValue_ShowsActualValue_WhenVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret123",
_clipboardService,
_ => { },
() => { });
// Act
sut.IsValueVisible = true;
// Assert
sut.DisplayValue.ShouldBe("secret123");
}
[Fact]
public void DisplayValue_LimitsMaskedLength_ToTwentyCharacters()
{
// Arrange
var longValue = new string('x', 50);
var sut = new SecretFormViewModel(
"API_KEY",
longValue,
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.DisplayValue.Length.ShouldBe(20);
sut.DisplayValue.ShouldBe(new string('\u2022', 20));
}
[Fact]
public void DisplayValue_HandlesEmptyValue()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.DisplayValue.ShouldBe("");
}
[Fact]
public void VisibilityButtonText_ShowsShow_WhenHidden()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.VisibilityButtonText.ShouldBe("Show");
}
[Fact]
public void VisibilityButtonText_ShowsHide_WhenVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act
sut.IsValueVisible = true;
// Assert
sut.VisibilityButtonText.ShouldBe("Hide");
}
[Fact]
public void ToggleVisibilityCommand_TogglesVisibility()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert - Toggle on
sut.ToggleVisibilityCommand.Execute(null);
sut.IsValueVisible.ShouldBeTrue();
// Act & Assert - Toggle off
sut.ToggleVisibilityCommand.Execute(null);
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public async Task CopyToClipboardCommand_CopiesValueToClipboard()
{
// Arrange
_clipboardService.SetTextAsync(Arg.Any<string>()).Returns(Task.CompletedTask);
var sut = new SecretFormViewModel(
"API_KEY",
"secret-to-copy",
_clipboardService,
_ => { },
() => { });
// Act
sut.CopyToClipboardCommand.Execute(null);
// Small delay to allow async command to complete
await Task.Delay(50);
// Assert
await _clipboardService.Received(1).SetTextAsync("secret-to-copy");
}
[Fact]
public void DeleteCommand_InvokesCallback()
{
// Arrange
var deleteCalled = false;
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => deleteCalled = true);
// Act
sut.DeleteCommand.Execute(null);
// Assert
deleteCalled.ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullKey()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel(null!, "value", _clipboardService, _ => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullClipboardService()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", null!, _ => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullValueChangedCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", _clipboardService, null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullDeleteCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", _clipboardService, _ => { }, null!));
}
[Fact]
public void Key_IsReadOnly()
{
// Assert - Verify Key property is get-only
var keyProperty = typeof(SecretFormViewModel).GetProperty(nameof(SecretFormViewModel.Key));
keyProperty!.CanWrite.ShouldBeFalse();
}
[Fact]
public void Value_DoesNotInvokeCallback_WhenValueUnchanged()
{
// Arrange
var callbackCount = 0;
var sut = new SecretFormViewModel(
"API_KEY",
"same-value",
_clipboardService,
_ => callbackCount++,
() => { });
// Act
sut.Value = "same-value"; // Same as initial value
// Assert
callbackCount.ShouldBe(0);
}
}
@@ -0,0 +1,783 @@
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Ui.Services;
using JdeScoping.ConfigManager.Ui.ViewModels;
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
using JdeScoping.DataSync.Configuration;
using Microsoft.Extensions.Logging;
using NSubstitute;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels;
public class MainWindowViewModelTests
{
private readonly IFileSystem _fileSystem;
private readonly IConfigFileService _configFileService;
private readonly IValidationService _validationService;
private readonly IBackupService _backupService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly IDialogService _dialogService;
private readonly ISecureStoreManager _secureStoreManager;
private readonly IClipboardService _clipboardService;
private readonly IRuntimeConfigValidationService _runtimeValidationService;
private readonly IConnectionTestService _connectionTestService;
private readonly ILogger<MainWindowViewModel> _logger;
public MainWindowViewModelTests()
{
_fileSystem = Substitute.For<IFileSystem>();
_configFileService = Substitute.For<IConfigFileService>();
_validationService = Substitute.For<IValidationService>();
_backupService = Substitute.For<IBackupService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
_dialogService = Substitute.For<IDialogService>();
_secureStoreManager = Substitute.For<ISecureStoreManager>();
_clipboardService = Substitute.For<IClipboardService>();
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
_connectionTestService = Substitute.For<IConnectionTestService>();
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
.Returns(new ValidationResult());
_validationService.ValidatePipelines(Arg.Any<Dictionary<string, EtlPipelineConfig>>())
.Returns(new ValidationResult());
}
[Fact]
public void SelectingDataSyncNode_LoadsDataSyncFormViewModel()
{
// Arrange
var config = new ConfigModel { DataSync = new DataSyncSection { MaxDegreeOfParallelism = 8 } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var dataSyncNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "DataSync");
// Act
sut.SelectedNode = dataSyncNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<DataSyncFormViewModel>();
((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism.ShouldBe(8);
}
[Fact]
public void SelectingDataAccessNode_LoadsDataAccessFormViewModel()
{
// Arrange
var config = new ConfigModel { DataAccess = new DataAccessSection { ProductionSchema = "custom" } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var dataAccessNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "DataAccess");
// Act
sut.SelectedNode = dataAccessNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<DataAccessFormViewModel>();
((DataAccessFormViewModel)sut.SelectedFormViewModel!).ProductionSchema.ShouldBe("custom");
}
[Fact]
public void SelectingAuthNode_LoadsAuthFormViewModel()
{
// Arrange
var config = new ConfigModel { Auth = new AuthSection { CookieName = "TestCookie" } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var authNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "Auth");
// Act
sut.SelectedNode = authNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<AuthFormViewModel>();
((AuthFormViewModel)sut.SelectedFormViewModel!).CookieName.ShouldBe("TestCookie");
}
[Fact]
public void SelectingLdapNode_LoadsLdapFormViewModel()
{
// Arrange
var config = new ConfigModel { Ldap = new LdapSection { GroupDn = "CN=TestGroup" } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var ldapNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "Ldap");
// Act
sut.SelectedNode = ldapNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<LdapFormViewModel>();
((LdapFormViewModel)sut.SelectedFormViewModel!).GroupDn.ShouldBe("CN=TestGroup");
}
[Fact]
public void SelectingSearchNode_LoadsSearchFormViewModel()
{
// Arrange
var config = new ConfigModel { Search = new SearchSection { MaxResultRows = 50000 } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var searchNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "Search");
// Act
sut.SelectedNode = searchNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<SearchFormViewModel>();
((SearchFormViewModel)sut.SelectedFormViewModel!).MaxResultRows.ShouldBe(50000);
}
[Fact]
public void SelectingExcelExportNode_LoadsExcelExportFormViewModel()
{
// Arrange
var config = new ConfigModel { ExcelExport = new ExcelExportSection { TimezoneId = "America/New_York" } };
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var excelNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "ExcelExport");
// Act
sut.SelectedNode = excelNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<ExcelExportFormViewModel>();
((ExcelExportFormViewModel)sut.SelectedFormViewModel!).SelectedTimezone.ShouldBe("America/New_York");
}
[Fact]
public void SelectingPipelineNode_LoadsPipelineEditorViewModel()
{
// Arrange
var config = new ConfigModel();
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["WorkOrders"] = new EtlPipelineConfig
{
Name = "WorkOrders",
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder_Curr" }
}
};
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, pipelines);
var pipelineNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "WorkOrders");
// Act
sut.SelectedNode = pipelineNode;
// Assert
sut.SelectedFormViewModel.ShouldBeOfType<PipelineEditorViewModel>();
var pipelineEditor = (PipelineEditorViewModel)sut.SelectedFormViewModel!;
pipelineEditor.Name.ShouldBe("WorkOrders");
pipelineEditor.Source.Connection.ShouldBe("jde");
}
[Fact]
public void ModifyingFormProperty_SetsHasUnsavedChanges()
{
// Arrange
var config = new ConfigModel();
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var dataSyncNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "DataSync");
sut.SelectedNode = dataSyncNode;
// Act
((DataSyncFormViewModel)sut.SelectedFormViewModel!).BatchSize = 10000;
// Assert
sut.HasUnsavedChanges.ShouldBeTrue();
}
[Fact]
public void ModifyingFormProperty_MarksNodeAsModified()
{
// Arrange
var config = new ConfigModel();
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var dataSyncNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "DataSync");
sut.SelectedNode = dataSyncNode;
// Act
((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism = 16;
// Assert
dataSyncNode.IsModified.ShouldBeTrue();
}
[Fact]
public void SelectingFolderNode_SetsSelectedFormViewModelToNull()
{
// Arrange
var config = new ConfigModel();
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var folderNode = sut.TreeNodes.First(); // Settings folder
// Act
sut.SelectedNode = folderNode;
// Assert
sut.SelectedFormViewModel.ShouldBeNull();
}
[Fact]
public void SelectingNull_SetsSelectedFormViewModelToNull()
{
// Arrange
var config = new ConfigModel();
var sut = CreateViewModel();
sut.LoadConfigForTesting(config, null);
var dataSyncNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "DataSync");
sut.SelectedNode = dataSyncNode;
// Act
sut.SelectedNode = null;
// Assert
sut.SelectedFormViewModel.ShouldBeNull();
}
[Fact]
public void LoadConfigForTesting_BuildsTreeNodes()
{
// Arrange
var config = new ConfigModel();
var sut = CreateViewModel();
// Act
sut.LoadConfigForTesting(config, null);
// Assert
// Without a configured/open SecureStore, only Settings and Pipelines appear
sut.TreeNodes.Count.ShouldBe(2); // Settings, Pipelines (no Secure Store when not configured)
sut.TreeNodes[0].Name.ShouldBe("Settings");
sut.TreeNodes[0].Children.Count.ShouldBe(7); // ConnectionStrings, DataSync, DataAccess, Auth, Ldap, Search, ExcelExport
sut.TreeNodes[1].Name.ShouldBe("Pipelines");
}
[Fact]
public void LoadConfigForTesting_WithPipelines_BuildsPipelineNodes()
{
// Arrange
var config = new ConfigModel();
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["Pipeline1"] = new EtlPipelineConfig { Name = "Pipeline1" },
["Pipeline2"] = new EtlPipelineConfig { Name = "Pipeline2" }
};
var sut = CreateViewModel();
// Act
sut.LoadConfigForTesting(config, pipelines);
// Assert
sut.TreeNodes[1].Name.ShouldBe("Pipelines");
sut.TreeNodes[1].Children.Count.ShouldBe(2);
}
[Fact]
public async Task OpenFolderCommand_UsesFilePicker_AndDerivesFolder()
{
// Arrange
var expectedFilePath = "/path/to/folder/appsettings.json";
var expectedFolder = "/path/to/folder";
var config = new ConfigModel();
// Ensure auto-discovery doesn't load config
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowFilePickerAsync(Arg.Any<string?>())
.Returns(expectedFilePath);
_configFileService.LoadAppSettingsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(config);
var sut = CreateViewModel();
// Wait for constructor async init to complete
await Task.Delay(50);
_configFileService.ClearReceivedCalls();
// Act
sut.OpenFolderCommand.Execute(null);
// Give async command time to complete
await Task.Delay(100);
// Assert
await _dialogService.Received(1).ShowFilePickerAsync("Select Configuration File");
await _configFileService.Received(1).LoadAppSettingsAsync(
Arg.Is<string>(s => s.Contains(expectedFolder)),
Arg.Any<CancellationToken>());
sut.ConfigFolderPath.ShouldBe(expectedFolder);
}
[Fact]
public async Task OpenFolderCommand_WhenCancelled_DoesNotLoadConfig()
{
// Arrange
// Ensure auto-discovery doesn't load config
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowFilePickerAsync(Arg.Any<string?>())
.Returns((string?)null);
var sut = CreateViewModel();
var originalPath = sut.ConfigFolderPath;
// Wait for constructor async init to complete
await Task.Delay(50);
_configFileService.ClearReceivedCalls();
// Act
sut.OpenFolderCommand.Execute(null);
// Give async command time to complete
await Task.Delay(100);
// Assert
await _dialogService.Received(1).ShowFilePickerAsync(Arg.Any<string?>());
await _configFileService.DidNotReceive().LoadAppSettingsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
sut.ConfigFolderPath.ShouldBe(originalPath);
}
[Fact]
public async Task SaveCommand_SavesAppSettings()
{
// Arrange
var testFolderPath = "/test/config";
var config = new ConfigModel();
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, null);
// Simulate setting the config folder path and marking as changed
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, testFolderPath);
sut.HasUnsavedChanges = true;
// Act
sut.SaveCommand.Execute(null);
await Task.Delay(100);
// Assert - File.Exists is a static call so backup may not be called, but SaveAppSettings should be
await _configFileService.Received().SaveAppSettingsAsync(Arg.Any<string>(), Arg.Any<ConfigModel>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task SaveCommand_ResetsHasUnsavedChanges()
{
// Arrange
var testFolderPath = Path.GetTempPath(); // Use real temp path so Directory.CreateDirectory works
var config = new ConfigModel
{
Pipelines = new PipelinesSection { ConfigDirectory = "Pipelines" }
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, null);
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, testFolderPath);
sut.HasUnsavedChanges = true;
// Act
sut.SaveCommand.Execute(null);
await Task.Delay(150);
// Assert - After successful save, HasUnsavedChanges should be false
sut.HasUnsavedChanges.ShouldBeFalse();
}
// Note: ValidateCommand tests are skipped because the Validate() method creates
// Avalonia SolidColorBrush objects which require UI thread access. These tests
// would need to use [AvaloniaFact] and run in the UI context, but the command
// validation logic is covered by the ValidationServiceTests.
[Fact]
public async Task AddSecretCommand_WhenStoreNotOpen_DoesNotShowDialog()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_secureStoreManager.IsStoreOpen.Returns(false);
var sut = CreateViewModel();
await Task.Delay(50);
// Act
sut.AddSecretCommand.Execute(null);
await Task.Delay(100);
// Assert - Command should not execute when store is not open
await _dialogService.DidNotReceive().ShowMessageAsync(
Arg.Is<string>(s => s == "Add Secret"),
Arg.Any<string>());
}
[Fact]
public async Task AddSecretCommand_WhenStoreOpen_ShowsDialog()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_secureStoreManager.IsStoreOpen.Returns(true);
var sut = CreateViewModel();
await Task.Delay(50);
// Act
sut.AddSecretCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
Arg.Is<string>(s => s == "Add Secret"),
Arg.Any<string>());
}
[Fact]
public async Task DeleteSecretCommand_WhenConfirmed_DeletesSecret()
{
// Arrange
var config = new ConfigModel
{
SecureStore = new SecureStoreSection
{
StorePath = "test.store",
KeyFilePath = "test.key"
}
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetKeys().Returns(new List<string> { "TestSecret" });
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = CreateViewModel();
await Task.Delay(50);
// Set ConfigFolderPath first - required for SecureStore node to be built
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, "/test/config");
sut.LoadConfigForTesting(config, null);
// Select the secret node
var secureStoreNode = sut.TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore);
secureStoreNode.ShouldNotBeNull();
var secretNode = secureStoreNode.Children.FirstOrDefault();
secretNode.ShouldNotBeNull();
sut.SelectedNode = secretNode;
// Act
sut.DeleteSecretCommand.Execute(null);
await Task.Delay(100);
// Assert
_secureStoreManager.Received().RemoveSecret("TestSecret");
}
[Fact]
public async Task DeleteSecretCommand_WhenCancelled_DoesNotDelete()
{
// Arrange
var config = new ConfigModel
{
SecureStore = new SecureStoreSection
{
StorePath = "test.store",
KeyFilePath = "test.key"
}
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetKeys().Returns(new List<string> { "TestSecret" });
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(false);
var sut = CreateViewModel();
await Task.Delay(50);
// Set ConfigFolderPath first - required for SecureStore node to be built
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, "/test/config");
sut.LoadConfigForTesting(config, null);
// Select the secret node
var secureStoreNode = sut.TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore);
secureStoreNode.ShouldNotBeNull();
var secretNode = secureStoreNode.Children.FirstOrDefault();
secretNode.ShouldNotBeNull();
sut.SelectedNode = secretNode;
// Act
sut.DeleteSecretCommand.Execute(null);
await Task.Delay(100);
// Assert
_secureStoreManager.DidNotReceive().RemoveSecret(Arg.Any<string>());
}
[Fact]
public async Task AddPipelineCommand_ShowsDialog_AndAddsPipeline()
{
// Arrange
var config = new ConfigModel();
var pipelines = new Dictionary<string, EtlPipelineConfig>();
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowInputDialogAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns("NewPipeline");
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, pipelines);
// Act
sut.AddPipelineCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowInputDialogAsync("New Pipeline", "Enter pipeline name:");
var pipelinesFolder = sut.TreeNodes.FirstOrDefault(n => n.Name == "Pipelines");
pipelinesFolder.ShouldNotBeNull();
pipelinesFolder.Children.Any(c => c.Name == "NewPipeline").ShouldBeTrue();
}
[Fact]
public async Task AddPipelineCommand_WithDuplicateName_ShowsError()
{
// Arrange
var config = new ConfigModel();
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["ExistingPipeline"] = new EtlPipelineConfig
{
Name = "ExistingPipeline",
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
Destination = new DestinationElement { Table = "TestTable" }
}
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowInputDialogAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns("ExistingPipeline");
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, pipelines);
// Act
sut.AddPipelineCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Error",
"Pipeline 'ExistingPipeline' already exists.");
}
[Fact]
public async Task DeletePipelineCommand_WhenConfirmed_RemovesPipelineFromTree()
{
// Arrange
var config = new ConfigModel
{
Pipelines = new PipelinesSection { ConfigDirectory = "Pipelines" }
};
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["TestPipeline"] = new EtlPipelineConfig
{
Name = "TestPipeline",
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
Destination = new DestinationElement { Table = "TestTable" }
}
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, pipelines);
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, "/test/config");
// Select the pipeline node
var pipelineNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "TestPipeline");
sut.SelectedNode = pipelineNode;
// Act
sut.DeletePipelineCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowConfirmationAsync(
"Delete Pipeline",
"Are you sure you want to delete pipeline 'TestPipeline'?");
// Pipeline should be removed from tree
var pipelinesFolder = sut.TreeNodes.First(n => n.Name == "Pipelines");
pipelinesFolder.Children.Any(c => c.SectionKey == "TestPipeline").ShouldBeFalse();
}
[Fact]
public async Task DeletePipelineCommand_WhenCancelled_DoesNotDelete()
{
// Arrange
var config = new ConfigModel();
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["TestPipeline"] = new EtlPipelineConfig
{
Name = "TestPipeline",
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
Destination = new DestinationElement { Table = "TestTable" }
}
};
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(false);
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, pipelines);
// Select the pipeline node
var pipelineNode = sut.TreeNodes
.SelectMany(n => n.Children)
.First(n => n.SectionKey == "TestPipeline");
sut.SelectedNode = pipelineNode;
var originalChildCount = sut.TreeNodes
.First(n => n.Name == "Pipelines").Children.Count;
// Act
sut.DeletePipelineCommand.Execute(null);
await Task.Delay(100);
// Assert
await _configFileService.DidNotReceive().DeletePipelineFileAsync(Arg.Any<string>());
sut.TreeNodes.First(n => n.Name == "Pipelines").Children.Count.ShouldBe(originalChildCount);
}
[Fact]
public async Task ValidateRuntimeConfigCommand_CallsRuntimeValidationService()
{
// Arrange
var testFolderPath = "/test/config";
var config = new ConfigModel();
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_runtimeValidationService.ValidateRuntimeConfig(Arg.Any<string>())
.Returns(new List<RuntimeValidationResult>());
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, null);
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, testFolderPath);
// Act
sut.ValidateRuntimeConfigCommand.Execute(null);
await Task.Delay(100);
// Assert
_runtimeValidationService.Received().ValidateRuntimeConfig(testFolderPath);
await _dialogService.Received().ShowValidationResultsAsync(
Arg.Any<ValidationResult>(),
Arg.Any<ValidationResult>());
}
[Fact]
public async Task ValidateRuntimeConfigCommand_WithErrors_ShowsErrorsInDialog()
{
// Arrange
var testFolderPath = "/test/config";
var config = new ConfigModel();
var runtimeResult = new RuntimeValidationResult
{
ValidatorName = "TestValidator"
};
runtimeResult.Errors.Add("Test runtime error");
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
_runtimeValidationService.ValidateRuntimeConfig(Arg.Any<string>())
.Returns(new List<RuntimeValidationResult> { runtimeResult });
var sut = CreateViewModel();
await Task.Delay(50);
sut.LoadConfigForTesting(config, null);
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
configFolderProperty!.SetValue(sut, testFolderPath);
// Act
sut.ValidateRuntimeConfigCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowValidationResultsAsync(
Arg.Is<ValidationResult>(r => r.Errors.Count > 0),
Arg.Any<ValidationResult>());
}
private MainWindowViewModel CreateViewModel()
{
return new MainWindowViewModel(
_fileSystem,
_configFileService,
_validationService,
_backupService,
_autoDiscoveryService,
_dialogService,
_secureStoreManager,
_clipboardService,
_runtimeValidationService,
_connectionTestService,
_logger);
}
}
@@ -0,0 +1,756 @@
using System.Text.Json;
using JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels.PipelineSteps;
public class ColumnDropTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesColumnsFromConfig()
{
// Arrange
var element = CreateColumnDropElement("Col1", "Col2", "Col3");
// Act
var sut = new ColumnDropTransformerViewModel(element, () => { });
// Assert
var columns = sut.GetColumns();
columns.ShouldContain("Col1");
columns.ShouldContain("Col2");
columns.ShouldContain("Col3");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesEmpty()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnDrop" };
// Act
var sut = new ColumnDropTransformerViewModel(element, () => { });
// Assert
sut.GetColumns().ShouldBeEmpty();
}
[Fact]
public void Constructor_Default_InitializesEmpty()
{
// Act
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.GetColumns().ShouldBeEmpty();
sut.ColumnsText.ShouldBe(string.Empty);
}
[Fact]
public void ColumnsText_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnDropTransformerViewModel(() => onChangedCalled = true);
// Act
sut.ColumnsText = "Column1\nColumn2";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void GetColumns_ReturnsNewlineSeparatedColumns()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\nCol2\nCol3";
// Act
var columns = sut.GetColumns();
// Assert
columns.Count.ShouldBe(3);
columns[0].ShouldBe("Col1");
columns[1].ShouldBe("Col2");
columns[2].ShouldBe("Col3");
}
[Fact]
public void GetColumns_TrimsWhitespace()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = " Col1 \n Col2 ";
// Act
var columns = sut.GetColumns();
// Assert
columns[0].ShouldBe("Col1");
columns[1].ShouldBe("Col2");
}
[Fact]
public void GetColumns_IgnoresEmptyLines()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\n\n\nCol2";
// Act
var columns = sut.GetColumns();
// Assert
columns.Count.ShouldBe(2);
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "DropMe\nAlsoDropMe";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("ColumnDrop");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsColumnDrop()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("ColumnDrop");
}
[Fact]
public void DisplayName_ReturnsColumnDrop()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("Column Drop");
}
[Fact]
public void Summary_WithColumns_ShowsCount()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\nCol2\nCol3";
// Assert
sut.Summary.ShouldBe("Drop 3 columns");
}
[Fact]
public void Summary_WithNoColumns_ShowsNoColumns()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("No columns");
}
private static TransformElement CreateColumnDropElement(params string[] columns)
{
var config = new { columns };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "ColumnDrop",
Config = doc.RootElement.Clone()
};
}
}
public class ColumnRenameTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesMappingsFromConfig()
{
// Arrange
var element = CreateColumnRenameElement(("OldName", "NewName"), ("Source", "Target"));
// Act
var sut = new ColumnRenameTransformerViewModel(element, () => { });
// Assert
sut.Mappings.Count.ShouldBe(2);
sut.Mappings.ShouldContain(m => m.OldName == "OldName" && m.NewName == "NewName");
sut.Mappings.ShouldContain(m => m.OldName == "Source" && m.NewName == "Target");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesEmpty()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnRename" };
// Act
var sut = new ColumnRenameTransformerViewModel(element, () => { });
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void Constructor_Default_InitializesEmpty()
{
// Act
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void AddMapping_AddsNewMapping()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Act
sut.AddMapping();
// Assert
sut.Mappings.Count.ShouldBe(1);
sut.Mappings[0].OldName.ShouldBe("");
sut.Mappings[0].NewName.ShouldBe("");
}
[Fact]
public void AddMapping_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnRenameTransformerViewModel(() => onChangedCalled = true);
// Act
sut.AddMapping();
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void RemoveMapping_RemovesMapping()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
var mapping = sut.Mappings[0];
// Act
sut.RemoveMapping(mapping);
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void RemoveMapping_InvokesOnChanged()
{
// Arrange
var onChangedCount = 0;
var sut = new ColumnRenameTransformerViewModel(() => onChangedCount++);
sut.AddMapping();
var mapping = sut.Mappings[0];
onChangedCount = 0;
// Act
sut.RemoveMapping(mapping);
// Assert
onChangedCount.ShouldBe(1);
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
sut.Mappings[0].OldName = "OldCol";
sut.Mappings[0].NewName = "NewCol";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("ColumnRename");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsColumnRename()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("ColumnRename");
}
[Fact]
public void DisplayName_ReturnsColumnRename()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("Column Rename");
}
[Fact]
public void Summary_WithMappings_ShowsCount()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
sut.AddMapping();
// Assert
sut.Summary.ShouldBe("Rename 2 columns");
}
[Fact]
public void Summary_WithNoMappings_ShowsNoMappings()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("No mappings");
}
private static TransformElement CreateColumnRenameElement(params (string oldName, string newName)[] mappings)
{
var dict = mappings.ToDictionary(m => m.oldName, m => m.newName);
var config = new { mappings = dict };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "ColumnRename",
Config = doc.RootElement.Clone()
};
}
}
public class ColumnMappingViewModelTests
{
[Fact]
public void Constructor_SetsProperties()
{
// Act
var sut = new ColumnMappingViewModel("OldName", "NewName", () => { });
// Assert
sut.OldName.ShouldBe("OldName");
sut.NewName.ShouldBe("NewName");
}
[Fact]
public void OldName_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnMappingViewModel("Old", "New", () => onChangedCalled = true);
// Act
sut.OldName = "Updated";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void NewName_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnMappingViewModel("Old", "New", () => onChangedCalled = true);
// Act
sut.NewName = "Updated";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void OldName_Setter_HandlesNull()
{
// Arrange
var sut = new ColumnMappingViewModel("Old", "New", () => { });
// Act
sut.OldName = null!;
// Assert
sut.OldName.ShouldBe(string.Empty);
}
[Fact]
public void NewName_Setter_HandlesNull()
{
// Arrange
var sut = new ColumnMappingViewModel("Old", "New", () => { });
// Act
sut.NewName = null!;
// Assert
sut.NewName.ShouldBe(string.Empty);
}
}
public class JdeDateTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesPropertiesFromConfig()
{
// Arrange
var element = CreateJdeDateElement("WADDJ", "WADTM", "CompletionDate");
// Act
var sut = new JdeDateTransformerViewModel(element, () => { });
// Assert
sut.DateColumn.ShouldBe("WADDJ");
sut.TimeColumn.ShouldBe("WADTM");
sut.OutputColumn.ShouldBe("CompletionDate");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesNull()
{
// Arrange
var element = new TransformElement { TransformType = "JdeDate" };
// Act
var sut = new JdeDateTransformerViewModel(element, () => { });
// Assert
sut.DateColumn.ShouldBeNull();
sut.TimeColumn.ShouldBeNull();
sut.OutputColumn.ShouldBeNull();
}
[Fact]
public void Constructor_Default_InitializesNull()
{
// Act
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.DateColumn.ShouldBeNull();
sut.TimeColumn.ShouldBeNull();
sut.OutputColumn.ShouldBeNull();
}
[Fact]
public void DateColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.DateColumn = "WADDJ";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void TimeColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.TimeColumn = "WADTM";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void OutputColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.OutputColumn = "ResultDate";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
sut.DateColumn = "DateCol";
sut.TimeColumn = "TimeCol";
sut.OutputColumn = "Output";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("JdeDate");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsJdeDate()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("JdeDate");
}
[Fact]
public void DisplayName_ReturnsJdeDateConvert()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("JDE Date Convert");
}
[Fact]
public void Summary_WithOutputColumn_ShowsOutputColumn()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
sut.OutputColumn = "CompletionDate";
// Assert
sut.Summary.ShouldContain("CompletionDate");
}
[Fact]
public void Summary_WithNoOutputColumn_ShowsConfigure()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("Configure...");
}
private static TransformElement CreateJdeDateElement(string? dateColumn, string? timeColumn, string? outputColumn)
{
var config = new { dateColumn, timeColumn, outputColumn };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "JdeDate",
Config = doc.RootElement.Clone()
};
}
}
public class TransformerFactoryTests
{
[Fact]
public void Create_ColumnDrop_ReturnsColumnDropViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnDrop" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void Create_ColumnRename_ReturnsColumnRenameViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnRename" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void Create_JdeDate_ReturnsJdeDateViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "JdeDate" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<JdeDateTransformerViewModel>();
}
[Fact]
public void Create_Regex_ReturnsRegexViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "Regex" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<RegexTransformerViewModel>();
}
[Fact]
public void Create_CaseInsensitive_WorksCorrectly()
{
// Arrange
var element = new TransformElement { TransformType = "COLUMNDROP" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void Create_UnknownType_ReturnsNull()
{
// Arrange
var element = new TransformElement { TransformType = "UnknownType" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void Create_NullType_ReturnsNull()
{
// Arrange
var element = new TransformElement { TransformType = null };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void CreateNew_ColumnDrop_ReturnsNewColumnDropViewModel()
{
// Act
var result = TransformerFactory.CreateNew("ColumnDrop", () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void CreateNew_ColumnRename_ReturnsNewColumnRenameViewModel()
{
// Act
var result = TransformerFactory.CreateNew("ColumnRename", () => { });
// Assert
result.ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void CreateNew_JdeDate_ReturnsNewJdeDateViewModel()
{
// Act
var result = TransformerFactory.CreateNew("JdeDate", () => { });
// Assert
result.ShouldBeOfType<JdeDateTransformerViewModel>();
}
[Fact]
public void CreateNew_Regex_ReturnsNewRegexViewModel()
{
// Act
var result = TransformerFactory.CreateNew("Regex", () => { });
// Assert
result.ShouldBeOfType<RegexTransformerViewModel>();
}
[Fact]
public void CreateNew_CaseInsensitive_WorksCorrectly()
{
// Act
var result = TransformerFactory.CreateNew("columndrop", () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void CreateNew_UnknownType_ReturnsNull()
{
// Act
var result = TransformerFactory.CreateNew("UnknownType", () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void CreateNew_NullType_ReturnsNull()
{
// Act
var result = TransformerFactory.CreateNew(null!, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void AvailableTypes_ContainsExpectedTypes()
{
// Assert
TransformerFactory.AvailableTypes.ShouldContain("ColumnDrop");
TransformerFactory.AvailableTypes.ShouldContain("ColumnRename");
TransformerFactory.AvailableTypes.ShouldContain("JdeDate");
TransformerFactory.AvailableTypes.ShouldContain("Regex");
}
[Fact]
public void AvailableTypes_HasFourTypes()
{
// Assert
TransformerFactory.AvailableTypes.Count.ShouldBe(4);
}
}
@@ -0,0 +1,231 @@
using JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
using JdeScoping.DataSync.Configuration;
using System.Text.Json;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels;
public class RegexTransformerViewModelTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private static TransformElement CreateElement(object config)
{
var json = JsonSerializer.Serialize(config, JsonOptions);
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "Regex",
Config = doc.RootElement.Clone()
};
}
[Fact]
public void Constructor_FromElement_LoadsAllProperties()
{
// Arrange
var element = CreateElement(new
{
columnName = "BatchID",
pattern = "^IIS_",
replacement = "",
ignoreCase = true,
nonMatchBehavior = "ReturnEmpty"
});
// Act
var vm = new RegexTransformerViewModel(element, () => { });
// Assert
Assert.Equal("BatchID", vm.ColumnName);
Assert.Equal("^IIS_", vm.Pattern);
Assert.Equal("", vm.Replacement);
Assert.True(vm.IsFindReplaceMode);
Assert.True(vm.IgnoreCase);
Assert.Equal(NonMatchBehavior.ReturnEmpty, vm.NonMatchBehavior);
}
[Fact]
public void Constructor_FromElement_MatchExtractMode_WhenReplacementNull()
{
// Arrange
var element = CreateElement(new
{
columnName = "Code",
pattern = @"(\d+)"
});
// Act
var vm = new RegexTransformerViewModel(element, () => { });
// Assert
Assert.False(vm.IsFindReplaceMode);
Assert.True(vm.IsMatchExtractMode);
}
[Fact]
public void ToModel_SerializesCorrectly_FindReplaceMode()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
ColumnName = "BatchID",
Pattern = "^IIS_",
Replacement = "",
IsFindReplaceMode = true,
IgnoreCase = true,
NonMatchBehavior = NonMatchBehavior.KeepOriginal
};
// Act
var element = vm.ToModel();
// Assert
Assert.Equal("Regex", element.TransformType);
Assert.True(element.Config.HasValue);
// Parse the config to verify
var config = element.Config!.Value;
Assert.Equal("BatchID", config.GetProperty("columnName").GetString());
Assert.Equal("^IIS_", config.GetProperty("pattern").GetString());
Assert.Equal("", config.GetProperty("replacement").GetString());
Assert.True(config.GetProperty("ignoreCase").GetBoolean());
}
[Fact]
public void ToModel_SerializesCorrectly_MatchExtractMode()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
ColumnName = "Code",
Pattern = @"(\d+)",
IsFindReplaceMode = false
};
// Act
var element = vm.ToModel();
// Assert
Assert.True(element.Config.HasValue);
var config = element.Config!.Value;
// replacement should be null in Match & Extract mode
Assert.True(config.TryGetProperty("replacement", out var replacement));
Assert.Equal(JsonValueKind.Null, replacement.ValueKind);
}
[Fact]
public void TestPatternCommand_ValidPattern_ShowsResult()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
Pattern = "^IIS_",
Replacement = "",
IsFindReplaceMode = true,
TestInput = "IIS_12345"
};
// Act
vm.TestPatternCommand.Execute(null);
// Assert
Assert.True(vm.HasTestResult);
Assert.False(vm.HasTestError);
Assert.Equal("12345", vm.TestResultValue);
Assert.Equal("Output", vm.TestResultLabel);
Assert.Equal("✓", vm.TestResultIcon);
}
[Fact]
public void TestPatternCommand_InvalidPattern_ShowsError()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
Pattern = "[invalid(regex",
Replacement = "",
TestInput = "test"
};
// Act
vm.TestPatternCommand.Execute(null);
// Assert
Assert.False(vm.HasTestResult);
Assert.True(vm.HasTestError);
Assert.NotEmpty(vm.TestErrorMessage);
}
[Fact]
public void TestPatternCommand_MatchExtract_NoMatch_ShowsNonMatchBehavior()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
Pattern = @"(\d+)",
IsFindReplaceMode = false,
NonMatchBehavior = NonMatchBehavior.ReturnNull,
TestInput = "NoNumbers"
};
// Act
vm.TestPatternCommand.Execute(null);
// Assert
Assert.True(vm.HasTestResult);
Assert.Equal("No Match", vm.TestResultLabel);
Assert.Equal("(null)", vm.TestResultValue);
}
[Fact]
public void ModeSwitch_UpdatesPatternHelpText()
{
// Arrange
var vm = new RegexTransformerViewModel(() => { })
{
IsFindReplaceMode = true
};
var findReplaceHelp = vm.PatternHelpText;
// Act
vm.IsFindReplaceMode = false;
var matchExtractHelp = vm.PatternHelpText;
// Assert
Assert.NotEqual(findReplaceHelp, matchExtractHelp);
Assert.Contains("capture group", matchExtractHelp, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Summary_ShowsColumnAndMode()
{
// Arrange & Act
var vm = new RegexTransformerViewModel(() => { })
{
ColumnName = "BatchID",
IsFindReplaceMode = true
};
// Assert
Assert.Contains("BatchID", vm.Summary);
Assert.Contains("Replace", vm.Summary);
}
[Fact]
public void PropertyChange_NotifiesChanged()
{
// Arrange
var changedCalled = false;
var vm = new RegexTransformerViewModel(() => changedCalled = true);
// Act
vm.ColumnName = "NewColumn";
// Assert
Assert.True(changedCalled);
}
}
@@ -0,0 +1,283 @@
using JdeScoping.ConfigManager.Ui.ViewModels;
namespace JdeScoping.ConfigManager.Ui.Tests.ViewModels;
public class TreeNodeViewModelTests
{
[Fact]
public void Constructor_SetsProperties()
{
// Arrange & Act
var node = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection);
// Assert
node.Name.ShouldBe("DataSync");
node.Icon.ShouldBe("⟳");
node.NodeType.ShouldBe(TreeNodeType.SettingsSection);
}
[Fact]
public void IsModified_WhenSet_RaisesPropertyChanged()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
var propertyChangedRaised = false;
node.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(TreeNodeViewModel.IsModified))
propertyChangedRaised = true;
};
// Act
node.IsModified = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsExpanded_WhenSet_RaisesPropertyChanged()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
var propertyChangedRaised = false;
node.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(TreeNodeViewModel.IsExpanded))
propertyChangedRaised = true;
};
// Act
node.IsExpanded = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
node.IsExpanded.ShouldBeTrue();
}
[Fact]
public void IsSelected_WhenSet_RaisesPropertyChanged()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
var propertyChangedRaised = false;
node.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(TreeNodeViewModel.IsSelected))
propertyChangedRaised = true;
};
// Act
node.IsSelected = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
node.IsSelected.ShouldBeTrue();
}
[Fact]
public void ValidationState_WhenSetToValid_StatusIconIsCheckmark()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
// Act
node.ValidationState = ValidationState.Valid;
// Assert
node.StatusIcon.ShouldBe("✓");
}
[Fact]
public void ValidationState_WhenSetToWarning_StatusIconIsWarning()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
// Act
node.ValidationState = ValidationState.Warning;
// Assert
node.StatusIcon.ShouldBe("⚠");
}
[Fact]
public void ValidationState_WhenSetToError_StatusIconIsX()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
// Act
node.ValidationState = ValidationState.Error;
// Assert
node.StatusIcon.ShouldBe("✗");
}
[Fact]
public void ValidationState_WhenSetToUnknown_StatusIconIsEmpty()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
// Act
node.ValidationState = ValidationState.Unknown;
// Assert
node.StatusIcon.ShouldBe("");
}
[Fact]
public void ValidationState_WhenChanged_RaisesPropertyChangedForStatusIcon()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
var statusIconPropertyChanged = false;
node.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(TreeNodeViewModel.StatusIcon))
statusIconPropertyChanged = true;
};
// Act
node.ValidationState = ValidationState.Valid;
// Assert
statusIconPropertyChanged.ShouldBeTrue();
}
[Fact]
public void SectionKey_CanBeSetViaInitializer()
{
// Arrange & Act
var node = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection)
{
SectionKey = "DataSync"
};
// Assert
node.SectionKey.ShouldBe("DataSync");
}
[Fact]
public void Children_IsInitialized()
{
// Arrange & Act
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
// Assert
node.Children.ShouldNotBeNull();
node.Children.ShouldBeEmpty();
}
[Fact]
public void Children_CanAddChildNodes()
{
// Arrange
var parent = new TreeNodeViewModel("Settings", "📁", TreeNodeType.Folder);
var child = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection);
// Act
parent.Children.Add(child);
// Assert
parent.Children.Count.ShouldBe(1);
parent.Children[0].ShouldBe(child);
}
[Theory]
[InlineData(TreeNodeType.Folder)]
[InlineData(TreeNodeType.SettingsSection)]
[InlineData(TreeNodeType.Pipeline)]
public void NodeType_CanBeSetForAllTypes(TreeNodeType nodeType)
{
// Arrange & Act
var node = new TreeNodeViewModel("Test", "📁", nodeType);
// Assert
node.NodeType.ShouldBe(nodeType);
}
[Fact]
public void IsModified_WhenSetToSameValue_DoesNotRaisePropertyChanged()
{
// Arrange
var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder);
node.IsModified = true;
var propertyChangedRaised = false;
node.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(TreeNodeViewModel.IsModified))
propertyChangedRaised = true;
};
// Act
node.IsModified = true; // Same value
// Assert
propertyChangedRaised.ShouldBeFalse();
}
#region SecureStore Node Type Tests
[Theory]
[InlineData(TreeNodeType.SecureStore)]
[InlineData(TreeNodeType.Secret)]
public void Constructor_WithSecureStoreNodeTypes_SetsNodeTypeCorrectly(TreeNodeType nodeType)
{
// Arrange & Act
var sut = new TreeNodeViewModel("Test", "icon", nodeType);
// Assert
sut.NodeType.ShouldBe(nodeType);
}
[Fact]
public void StorePath_CanBeSetViaInitializer()
{
// Arrange & Act
var sut = new TreeNodeViewModel("Store", "🔒", TreeNodeType.SecureStore)
{
StorePath = "/path/to/store.secrets"
};
// Assert
sut.StorePath.ShouldBe("/path/to/store.secrets");
}
[Fact]
public void StorePath_DefaultsToNull()
{
// Arrange & Act
var sut = new TreeNodeViewModel("Store", "🔒", TreeNodeType.SecureStore);
// Assert
sut.StorePath.ShouldBeNull();
}
[Fact]
public void SecretKey_CanBeSetViaInitializer()
{
// Arrange & Act
var sut = new TreeNodeViewModel("MySecret", "🔑", TreeNodeType.Secret)
{
SecretKey = "ConnectionStrings:Database"
};
// Assert
sut.SecretKey.ShouldBe("ConnectionStrings:Database");
}
[Fact]
public void SecretKey_DefaultsToNull()
{
// Arrange & Act
var sut = new TreeNodeViewModel("MySecret", "🔑", TreeNodeType.Secret);
// Assert
sut.SecretKey.ShouldBeNull();
}
#endregion
}