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:
@@ -0,0 +1,3 @@
|
||||
global using Xunit;
|
||||
global using Shouldly;
|
||||
global using NSubstitute;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Utils\JdeScoping.ConfigManager.Cli\JdeScoping.ConfigManager.Cli.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Utils\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
global using Xunit;
|
||||
global using Shouldly;
|
||||
global using NSubstitute;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Utils\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Models;
|
||||
|
||||
public class ConnectionStringEntryTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateConnectionString_SqlServer_ProducesCorrectFormat()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost\\SQLEXPRESS",
|
||||
Database = "TestDb",
|
||||
UserId = "sa",
|
||||
Password = "secret123",
|
||||
Encrypt = "True",
|
||||
TrustServerCertificate = true,
|
||||
ConnectionTimeout = 60,
|
||||
ApplicationName = "TestApp"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldContain("Server=localhost\\SQLEXPRESS");
|
||||
result.ShouldContain("Database=TestDb");
|
||||
result.ShouldContain("User Id=sa");
|
||||
result.ShouldContain("Password=secret123");
|
||||
result.ShouldContain("Encrypt=True");
|
||||
result.ShouldContain("TrustServerCertificate=True");
|
||||
result.ShouldContain("Connection Timeout=60");
|
||||
result.ShouldContain("Application Name=TestApp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_SqlServer_OmitsDefaultTimeout()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
Database = "TestDb",
|
||||
UserId = "user",
|
||||
Password = "pass",
|
||||
ConnectionTimeout = 30 // Default value
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldNotContain("Connection Timeout=");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_Oracle_ProducesEZConnectFormat()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.Oracle,
|
||||
Host = "oracle-server.example.com",
|
||||
Port = 1522,
|
||||
ServiceName = "ORCL",
|
||||
UserId = "scott",
|
||||
Password = "tiger"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldContain("Data Source=//oracle-server.example.com:1522/ORCL");
|
||||
result.ShouldContain("User Id=scott");
|
||||
result.ShouldContain("Password=tiger");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_Generic_ReturnsRawString()
|
||||
{
|
||||
// Arrange
|
||||
var rawConnString = "Driver={ODBC Driver};Server=myserver;Database=mydb;";
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.Generic,
|
||||
RawConnectionString = rawConnString
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(rawConnString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
// Act
|
||||
var entry = new ConnectionStringEntry();
|
||||
|
||||
// Assert
|
||||
entry.Name.ShouldBe(string.Empty);
|
||||
entry.Provider.ShouldBe(ConnectionProvider.Generic);
|
||||
entry.Server.ShouldBeNull();
|
||||
entry.SqlServerPort.ShouldBeNull();
|
||||
entry.Database.ShouldBeNull();
|
||||
entry.UserId.ShouldBeNull();
|
||||
entry.Password.ShouldBeNull();
|
||||
entry.Encrypt.ShouldBe("True");
|
||||
entry.TrustServerCertificate.ShouldBeFalse();
|
||||
entry.ConnectionTimeout.ShouldBe(30);
|
||||
entry.ApplicationName.ShouldBeNull();
|
||||
entry.Host.ShouldBeNull();
|
||||
entry.Port.ShouldBe(1521);
|
||||
entry.ServiceName.ShouldBeNull();
|
||||
entry.RawConnectionString.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_SqlServer_WithPort_IncludesPortInServer()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
SqlServerPort = 1434,
|
||||
Database = "TestDb",
|
||||
UserId = "sa",
|
||||
Password = "secret123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldContain("Server=localhost,1434");
|
||||
result.ShouldContain("Database=TestDb");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_SqlServer_WithoutPort_NoPortInServer()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
SqlServerPort = null,
|
||||
Database = "TestDb",
|
||||
UserId = "sa",
|
||||
Password = "secret123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldContain("Server=localhost;");
|
||||
result.ShouldNotContain("Server=localhost,");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateConnectionString_SqlServer_WithZeroPort_NoPortInServer()
|
||||
{
|
||||
// Arrange
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
SqlServerPort = 0,
|
||||
Database = "TestDb",
|
||||
UserId = "sa",
|
||||
Password = "secret123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = entry.GenerateConnectionString();
|
||||
|
||||
// Assert
|
||||
result.ShouldContain("Server=localhost;");
|
||||
result.ShouldNotContain("Server=localhost,");
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
using System.Text.Json;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Models;
|
||||
|
||||
public class ConnectionStringsSectionConverterTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_StandardDictionaryFormat_ParsesAllConnections()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"LotFinder": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=test",
|
||||
"JDE": "Data Source=jde-server:1521/JDEPROD;User Id=jdeuser;Password=jdepass",
|
||||
"CMS": "Data Source=cms-server:1521/CMSPROD;User Id=cmsuser;Password=cmspass"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.Count.ShouldBe(3);
|
||||
|
||||
var lotFinder = section.Entries.First(e => e.Name == "LotFinder");
|
||||
lotFinder.Provider.ShouldBe(ConnectionProvider.SqlServer);
|
||||
lotFinder.Server.ShouldBe("localhost,1434"); // Port embedded in server, not parsed separately
|
||||
lotFinder.SqlServerPort.ShouldBeNull();
|
||||
lotFinder.Database.ShouldBe("ScopingTool");
|
||||
lotFinder.UserId.ShouldBe("sa");
|
||||
|
||||
var jde = section.Entries.First(e => e.Name == "JDE");
|
||||
jde.Provider.ShouldBe(ConnectionProvider.Oracle);
|
||||
jde.Host.ShouldBe("jde-server");
|
||||
jde.Port.ShouldBe(1521);
|
||||
jde.ServiceName.ShouldBe("JDEPROD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_SqlServerConnection_ParsesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"TestDb": "Server=myserver;Database=TestDB;User Id=testuser;Password=testpass;Encrypt=True;TrustServerCertificate=True;Connection Timeout=60;Application Name=TestApp"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.Count.ShouldBe(1);
|
||||
|
||||
var entry = section.Entries[0];
|
||||
entry.Name.ShouldBe("TestDb");
|
||||
entry.Provider.ShouldBe(ConnectionProvider.SqlServer);
|
||||
entry.Server.ShouldBe("myserver");
|
||||
entry.Database.ShouldBe("TestDB");
|
||||
entry.UserId.ShouldBe("testuser");
|
||||
entry.Password.ShouldBe("testpass");
|
||||
entry.Encrypt.ShouldBe("True");
|
||||
entry.TrustServerCertificate.ShouldBeTrue();
|
||||
entry.ConnectionTimeout.ShouldBe(60);
|
||||
entry.ApplicationName.ShouldBe("TestApp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_OracleConnection_ParsesHostPortService()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Oracle": "Data Source=//db-host:1523/PRODDB;User Id=orauser;Password=orapass"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.Count.ShouldBe(1);
|
||||
|
||||
var entry = section.Entries[0];
|
||||
entry.Name.ShouldBe("Oracle");
|
||||
entry.Provider.ShouldBe(ConnectionProvider.Oracle);
|
||||
entry.Host.ShouldBe("db-host");
|
||||
entry.Port.ShouldBe(1523);
|
||||
entry.ServiceName.ShouldBe("PRODDB");
|
||||
entry.UserId.ShouldBe("orauser");
|
||||
entry.Password.ShouldBe("orapass");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_EmptyObject_ReturnsEmptyEntries()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{}";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_ToStandardDictionaryFormat()
|
||||
{
|
||||
// Arrange
|
||||
var section = new ConnectionStringsSection
|
||||
{
|
||||
Entries = new List<ConnectionStringEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "TestDb",
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "myserver",
|
||||
Database = "TestDB",
|
||||
UserId = "testuser",
|
||||
Password = "testpass"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(section, JsonOptions);
|
||||
|
||||
// Assert
|
||||
json.ShouldNotBeNull();
|
||||
json.ShouldContain("\"TestDb\"");
|
||||
json.ShouldContain("Server=myserver");
|
||||
json.ShouldContain("Database=TestDB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesConnections()
|
||||
{
|
||||
// Arrange
|
||||
var original = new ConnectionStringsSection
|
||||
{
|
||||
Entries = new List<ConnectionStringEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Primary",
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "server1",
|
||||
Database = "DB1",
|
||||
UserId = "user1",
|
||||
Password = "pass1"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Secondary",
|
||||
Provider = ConnectionProvider.Oracle,
|
||||
Host = "oracle-host",
|
||||
Port = 1521,
|
||||
ServiceName = "ORCL",
|
||||
UserId = "user2",
|
||||
Password = "pass2"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(original, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized.Entries.Count.ShouldBe(2);
|
||||
|
||||
var primary = deserialized.Entries.First(e => e.Name == "Primary");
|
||||
primary.Provider.ShouldBe(ConnectionProvider.SqlServer);
|
||||
primary.Server.ShouldBe("server1");
|
||||
primary.Database.ShouldBe("DB1");
|
||||
|
||||
var secondary = deserialized.Entries.First(e => e.Name == "Secondary");
|
||||
secondary.Provider.ShouldBe(ConnectionProvider.Oracle);
|
||||
secondary.Host.ShouldBe("oracle-host");
|
||||
secondary.Port.ShouldBe(1521);
|
||||
secondary.ServiceName.ShouldBe("ORCL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_SqlServerWithEmbeddedPort_KeepsPortInServer()
|
||||
{
|
||||
// Arrange - connection string with port embedded in server name (legacy format)
|
||||
var json = """
|
||||
{
|
||||
"TestDb": "Server=localhost,1434;Database=TestDB;User Id=testuser;Password=testpass"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert - port is NOT parsed out, stays embedded in server name
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.Count.ShouldBe(1);
|
||||
|
||||
var entry = section.Entries[0];
|
||||
entry.Name.ShouldBe("TestDb");
|
||||
entry.Provider.ShouldBe(ConnectionProvider.SqlServer);
|
||||
entry.Server.ShouldBe("localhost,1434"); // Port stays in server name
|
||||
entry.SqlServerPort.ShouldBeNull(); // Port control not populated from parsing
|
||||
entry.Database.ShouldBe("TestDB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_SqlServerWithoutPort_ServerHasNoPort()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"TestDb": "Server=myserver;Database=TestDB;User Id=testuser;Password=testpass"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var section = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
section.ShouldNotBeNull();
|
||||
section.Entries.Count.ShouldBe(1);
|
||||
|
||||
var entry = section.Entries[0];
|
||||
entry.Server.ShouldBe("myserver");
|
||||
entry.SqlServerPort.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_SqlServerWithPort_EmbedsPortInServer()
|
||||
{
|
||||
// Arrange - entry with separate port control
|
||||
var original = new ConnectionStringsSection
|
||||
{
|
||||
Entries = new List<ConnectionStringEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "WithPort",
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
SqlServerPort = 1434,
|
||||
Database = "DB1",
|
||||
UserId = "user1",
|
||||
Password = "pass1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(original, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ConnectionStringsSection>(json, JsonOptions);
|
||||
|
||||
// Assert - after round trip, port is embedded in server name (not parsed back out)
|
||||
deserialized.ShouldNotBeNull();
|
||||
var entry = deserialized.Entries.First();
|
||||
entry.Server.ShouldBe("localhost,1434"); // Port embedded in server after round trip
|
||||
entry.SqlServerPort.ShouldBeNull(); // Port control not populated from parsing
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class AutoDiscoveryServiceTests : IDisposable
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly AutoDiscoveryService _sut;
|
||||
private readonly string? _originalEnvVar;
|
||||
|
||||
public AutoDiscoveryServiceTests()
|
||||
{
|
||||
_fileSystem = Substitute.For<IFileSystem>();
|
||||
_sut = new AutoDiscoveryService(_fileSystem);
|
||||
// Save original env var to restore later
|
||||
_originalEnvVar = Environment.GetEnvironmentVariable("JDESCOPING_CONFIG_PATH");
|
||||
|
||||
// Set up default Combine behavior to work like Path.Combine
|
||||
_fileSystem.Combine(Arg.Any<string[]>()).Returns(callInfo =>
|
||||
{
|
||||
var paths = callInfo.Arg<string[]>();
|
||||
return Path.Combine(paths);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Restore original env var
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", _originalEnvVar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenEnvVarSet_ReturnsEnvPath()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/custom/config");
|
||||
_fileSystem.DirectoryExists("/custom/config").Returns(true);
|
||||
_fileSystem.FileExists(Arg.Is<string>(s => s.Contains("appsettings.json") && s.Contains("/custom/config"))).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("/custom/config");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenEnvVarSetButDirectoryInvalid_SearchesOtherLocations()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/invalid/config");
|
||||
_fileSystem.DirectoryExists(Arg.Any<string>()).Returns(false);
|
||||
_fileSystem.FileExists(Arg.Any<string>()).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenEnvVarSetButNoAppSettings_SearchesOtherLocations()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/custom/config");
|
||||
// Directory exists but no appsettings.json
|
||||
_fileSystem.DirectoryExists("/custom/config").Returns(true);
|
||||
_fileSystem.FileExists(Arg.Any<string>()).Returns(false);
|
||||
// All other locations also don't have config
|
||||
_fileSystem.DirectoryExists(Arg.Is<string>(s => s != "/custom/config")).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null);
|
||||
_fileSystem.DirectoryExists(Arg.Any<string>()).Returns(false);
|
||||
_fileSystem.FileExists(Arg.Any<string>()).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenExeDirectoryHasConfig_ReturnsExeDirectory()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null);
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
|
||||
_fileSystem.DirectoryExists(exeDir).Returns(true);
|
||||
_fileSystem.FileExists(Arg.Is<string>(s => s.Contains(exeDir) && s.Contains("appsettings.json"))).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(exeDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenHostDirectoryHasConfig_ReturnsHostDirectory()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null);
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var hostDir = Path.Combine(exeDir, "..", "JdeScoping.Host");
|
||||
|
||||
// Exe directory doesn't have config
|
||||
_fileSystem.DirectoryExists(exeDir).Returns(false);
|
||||
|
||||
// Host directory has config
|
||||
_fileSystem.DirectoryExists(hostDir).Returns(true);
|
||||
_fileSystem.FileExists(Arg.Is<string>(s => s.Contains("JdeScoping.Host") && s.Contains("appsettings.json"))).Returns(true);
|
||||
|
||||
// Other directories don't have config
|
||||
_fileSystem.DirectoryExists(Arg.Is<string>(s => s != exeDir && s != hostDir)).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(hostDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WhenUserConfigDirectoryHasConfig_ReturnsUserConfigDirectory()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null);
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var hostDir = Path.Combine(exeDir, "..", "JdeScoping.Host");
|
||||
|
||||
// Determine user config path based on OS
|
||||
string userConfigDir;
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
userConfigDir = Path.Combine(localAppData, "JdeScoping");
|
||||
}
|
||||
else
|
||||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
userConfigDir = Path.Combine(home, ".jdescoping");
|
||||
}
|
||||
|
||||
// Exe directory doesn't have config
|
||||
_fileSystem.DirectoryExists(exeDir).Returns(false);
|
||||
|
||||
// Host directory doesn't have config
|
||||
_fileSystem.DirectoryExists(hostDir).Returns(false);
|
||||
|
||||
// User config directory has config
|
||||
_fileSystem.DirectoryExists(userConfigDir).Returns(true);
|
||||
_fileSystem.FileExists(Arg.Is<string>(s => s.Contains(userConfigDir) && s.Contains("appsettings.json"))).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(userConfigDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_SearchesPrioritizedOrder()
|
||||
{
|
||||
// Arrange - all locations have config, but env var should be returned first
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/env/config");
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
|
||||
// Both env var path and exe dir have valid config
|
||||
_fileSystem.DirectoryExists("/env/config").Returns(true);
|
||||
_fileSystem.DirectoryExists(exeDir).Returns(true);
|
||||
_fileSystem.FileExists(Arg.Any<string>()).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync();
|
||||
|
||||
// Assert - should return env var path, not exe dir
|
||||
result.ShouldBe("/env/config");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindConfigFolderAsync_WithCancellationToken_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/custom/config");
|
||||
_fileSystem.DirectoryExists("/custom/config").Returns(true);
|
||||
_fileSystem.FileExists(Arg.Any<string>()).Returns(true);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
// Act
|
||||
var result = await _sut.FindConfigFolderAsync(cts.Token);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("/custom/config");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class BackupServiceTests
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly BackupService _sut;
|
||||
|
||||
public BackupServiceTests()
|
||||
{
|
||||
_fileSystem = Substitute.For<IFileSystem>();
|
||||
_sut = new BackupService(_fileSystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBackupAsync_CreatesTimestampedBackup()
|
||||
{
|
||||
// Arrange
|
||||
var sourcePath = "/config/appsettings.json";
|
||||
_fileSystem.FileExists(sourcePath).Returns(true);
|
||||
_fileSystem.GetDirectoryName(sourcePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(sourcePath).Returns("appsettings");
|
||||
_fileSystem.Combine(Arg.Any<string[]>()).Returns(callInfo =>
|
||||
{
|
||||
var paths = callInfo.Arg<string[]>();
|
||||
return string.Join("/", paths);
|
||||
});
|
||||
|
||||
// Act
|
||||
var backupPath = await _sut.CreateBackupAsync(sourcePath);
|
||||
|
||||
// Assert
|
||||
backupPath.ShouldStartWith("/config/appsettings.");
|
||||
backupPath.ShouldEndWith(".bak");
|
||||
await _fileSystem.Received(1).CopyFileAsync(sourcePath, backupPath, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupOldBackupsAsync_KeepsOnlySpecifiedCount()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
|
||||
var backups = Enumerable.Range(1, 15)
|
||||
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
|
||||
.ToArray();
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backups));
|
||||
|
||||
// Mock GetFileNameWithoutExtension for each backup file
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var fileName = backup.Split('/').Last().Replace(".bak", "");
|
||||
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
|
||||
}
|
||||
|
||||
// Act
|
||||
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
|
||||
|
||||
// Assert - should delete 5 oldest backups
|
||||
await _fileSystem.Received(5).DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBackupsAsync_ReturnsBackupsSortedByTimestampDescending()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
|
||||
// Backups in random order
|
||||
var backups = new[]
|
||||
{
|
||||
"/config/appsettings.2026-01-15_120000.bak",
|
||||
"/config/appsettings.2026-01-10_120000.bak",
|
||||
"/config/appsettings.2026-01-20_120000.bak"
|
||||
};
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backups));
|
||||
|
||||
// Mock GetFileNameWithoutExtension for each backup file
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var fileName = backup.Split('/').Last().Replace(".bak", "");
|
||||
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetBackupsAsync(filePath);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Count.ShouldBe(3);
|
||||
|
||||
// Should be sorted descending by timestamp (newest first)
|
||||
result[0].Path.ShouldContain("2026-01-20");
|
||||
result[1].Path.ShouldContain("2026-01-15");
|
||||
result[2].Path.ShouldContain("2026-01-10");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBackupsAsync_WithNoBackups_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(Array.Empty<string>()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetBackupsAsync(filePath);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreBackupAsync_CopiesBackupToTarget()
|
||||
{
|
||||
// Arrange
|
||||
var backupPath = "/config/appsettings.2026-01-15_120000.bak";
|
||||
var targetPath = "/config/appsettings.json";
|
||||
|
||||
// Act
|
||||
await _sut.RestoreBackupAsync(backupPath, targetPath);
|
||||
|
||||
// Assert
|
||||
await _fileSystem.Received(1).CopyFileAsync(backupPath, targetPath, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupOldBackupsAsync_WithFewerThanKeepCount_DeletesNone()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
|
||||
// Only 3 backups, but keepCount is 10
|
||||
var backups = new[]
|
||||
{
|
||||
"/config/appsettings.2026-01-15_120000.bak",
|
||||
"/config/appsettings.2026-01-16_120000.bak",
|
||||
"/config/appsettings.2026-01-17_120000.bak"
|
||||
};
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backups));
|
||||
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var fileName = backup.Split('/').Last().Replace(".bak", "");
|
||||
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
|
||||
}
|
||||
|
||||
// Act
|
||||
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
|
||||
|
||||
// Assert - no files should be deleted
|
||||
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBackupAsync_WithNonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var sourcePath = "/config/nonexistent.json";
|
||||
_fileSystem.FileExists(sourcePath).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
await Should.ThrowAsync<FileNotFoundException>(
|
||||
() => _sut.CreateBackupAsync(sourcePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBackupsAsync_SkipsFilesWithInvalidTimestampFormat()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
|
||||
// Mix of valid and invalid backup filenames
|
||||
var backups = new[]
|
||||
{
|
||||
"/config/appsettings.2026-01-15_120000.bak", // Valid
|
||||
"/config/appsettings.invalid-format.bak", // Invalid
|
||||
"/config/appsettings.2026-01-16_120000.bak" // Valid
|
||||
};
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backups));
|
||||
|
||||
_fileSystem.GetFileNameWithoutExtension(backups[0]).Returns("appsettings.2026-01-15_120000");
|
||||
_fileSystem.GetFileNameWithoutExtension(backups[1]).Returns("appsettings.invalid-format");
|
||||
_fileSystem.GetFileNameWithoutExtension(backups[2]).Returns("appsettings.2026-01-16_120000");
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetBackupsAsync(filePath);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Count.ShouldBe(2); // Only valid backups
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupOldBackupsAsync_WithExactKeepCount_DeletesNone()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/config/appsettings.json";
|
||||
_fileSystem.GetDirectoryName(filePath).Returns("/config");
|
||||
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
|
||||
|
||||
// Exactly 5 backups with keepCount of 5
|
||||
var backups = Enumerable.Range(1, 5)
|
||||
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
|
||||
.ToArray();
|
||||
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backups));
|
||||
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var fileName = backup.Split('/').Last().Replace(".bak", "");
|
||||
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
|
||||
}
|
||||
|
||||
// Act
|
||||
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 5);
|
||||
|
||||
// Assert - no files should be deleted
|
||||
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class ConfigFileServiceTests
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ConfigFileService _sut;
|
||||
|
||||
public ConfigFileServiceTests()
|
||||
{
|
||||
_fileSystem = Substitute.For<IFileSystem>();
|
||||
_sut = new ConfigFileService(_fileSystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAppSettingsAsync_WithValidJson_ReturnsConfigModel()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"DataSync": {
|
||||
"Enabled": true,
|
||||
"MaxDegreeOfParallelism": 8
|
||||
}
|
||||
}
|
||||
""";
|
||||
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(json));
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadAppSettingsAsync("/config/appsettings.json");
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.DataSync.Enabled.ShouldBeTrue();
|
||||
result.DataSync.MaxDegreeOfParallelism.ShouldBe(8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAppSettingsAsync_WithInvalidJson_ThrowsWithHelpfulMessage()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{ invalid json }";
|
||||
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(json));
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<ConfigLoadException>(
|
||||
() => _sut.LoadAppSettingsAsync("/config/appsettings.json"));
|
||||
ex.Message.ShouldContain("parse");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadPipelineAsync_WithValidJson_ReturnsPipelineConfig()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"name": "TestPipeline",
|
||||
"isEnabled": true,
|
||||
"massSyncIntervalMinutes": 1440,
|
||||
"source": {
|
||||
"connectionStringName": "JDE",
|
||||
"query": "SELECT * FROM test"
|
||||
},
|
||||
"destination": {
|
||||
"connectionStringName": "LotFinder",
|
||||
"tableName": "TestTable"
|
||||
}
|
||||
}
|
||||
""";
|
||||
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(json));
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadPipelineAsync("/config/pipeline.test.json");
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Name.ShouldBe("TestPipeline");
|
||||
result.IsEnabled.ShouldBeTrue();
|
||||
result.MassSyncIntervalMinutes.ShouldBe(1440);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadPipelineAsync_WithInvalidJson_ThrowsConfigLoadException()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{ invalid json }";
|
||||
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(json));
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<ConfigLoadException>(
|
||||
() => _sut.LoadPipelineAsync("/config/pipeline.test.json"));
|
||||
ex.Message.ShouldContain("parse");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SavePipelineAsync_SerializesAndWritesPipeline()
|
||||
{
|
||||
// Arrange
|
||||
var path = "/config/pipeline.test.json";
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Name = "TestPipeline",
|
||||
IsEnabled = true,
|
||||
MassSyncIntervalMinutes = 1440
|
||||
};
|
||||
string? writtenContent = null;
|
||||
_fileSystem.WriteAllTextAsync(Arg.Any<string>(), Arg.Do<string>(c => writtenContent = c), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _sut.SavePipelineAsync(path, pipeline);
|
||||
|
||||
// Assert
|
||||
await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
writtenContent.ShouldNotBeNull();
|
||||
writtenContent.ShouldContain("TestPipeline");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllPipelinesAsync_WithMultiplePipelines_ReturnsAll()
|
||||
{
|
||||
// Arrange
|
||||
var directory = "/config/pipelines";
|
||||
var files = new[]
|
||||
{
|
||||
"/config/pipelines/pipeline.first.json",
|
||||
"/config/pipelines/pipeline.second.json"
|
||||
};
|
||||
|
||||
_fileSystem.DirectoryExists(directory).Returns(true);
|
||||
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(files));
|
||||
|
||||
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult("""{"name": "First", "isEnabled": true}"""));
|
||||
_fileSystem.ReadAllTextAsync(files[1], Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult("""{"name": "Second", "isEnabled": false}"""));
|
||||
|
||||
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.first");
|
||||
_fileSystem.GetFileNameWithoutExtension(files[1]).Returns("pipeline.second");
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadAllPipelinesAsync(directory);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Count.ShouldBe(2);
|
||||
result.ContainsKey("first").ShouldBeTrue();
|
||||
result.ContainsKey("second").ShouldBeTrue();
|
||||
result["first"].Name.ShouldBe("First");
|
||||
result["second"].IsEnabled.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllPipelinesAsync_WithNonExistentDirectory_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var directory = "/config/nonexistent";
|
||||
_fileSystem.DirectoryExists(directory).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadAllPipelinesAsync(directory);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletePipelineFileAsync_CallsFileSystemDelete()
|
||||
{
|
||||
// Arrange
|
||||
var path = "/config/pipeline.test.json";
|
||||
|
||||
// Act
|
||||
await _sut.DeletePipelineFileAsync(path);
|
||||
|
||||
// Assert
|
||||
await _fileSystem.Received(1).DeleteFileAsync(path, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAppSettingsAsync_SerializesAndWritesConfig()
|
||||
{
|
||||
// Arrange
|
||||
var path = "/config/appsettings.json";
|
||||
var config = new ConfigModel
|
||||
{
|
||||
DataSync = new DataSyncSection
|
||||
{
|
||||
Enabled = true,
|
||||
MaxDegreeOfParallelism = 16
|
||||
}
|
||||
};
|
||||
string? writtenContent = null;
|
||||
_fileSystem.WriteAllTextAsync(Arg.Any<string>(), Arg.Do<string>(c => writtenContent = c), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _sut.SaveAppSettingsAsync(path, config);
|
||||
|
||||
// Assert
|
||||
await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
writtenContent.ShouldNotBeNull();
|
||||
writtenContent.ShouldContain("enabled");
|
||||
writtenContent.ShouldContain("16");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllPipelinesAsync_SkipsInvalidPipelineFiles()
|
||||
{
|
||||
// Arrange
|
||||
var directory = "/config/pipelines";
|
||||
var files = new[]
|
||||
{
|
||||
"/config/pipelines/pipeline.valid.json",
|
||||
"/config/pipelines/pipeline.invalid.json"
|
||||
};
|
||||
|
||||
_fileSystem.DirectoryExists(directory).Returns(true);
|
||||
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(files));
|
||||
|
||||
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult("""{"name": "Valid", "isEnabled": true}"""));
|
||||
_fileSystem.ReadAllTextAsync(files[1], Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult("{ invalid json }"));
|
||||
|
||||
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.valid");
|
||||
_fileSystem.GetFileNameWithoutExtension(files[1]).Returns("pipeline.invalid");
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadAllPipelinesAsync(directory);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Count.ShouldBe(1);
|
||||
result.ContainsKey("valid").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllPipelinesAsync_AssignsPipelineNameFromFilename_WhenNameIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var directory = "/config/pipelines";
|
||||
var files = new[] { "/config/pipelines/pipeline.unnamed.json" };
|
||||
|
||||
_fileSystem.DirectoryExists(directory).Returns(true);
|
||||
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(files));
|
||||
|
||||
// Pipeline with empty name
|
||||
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult("""{"name": "", "isEnabled": true}"""));
|
||||
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.unnamed");
|
||||
|
||||
// Act
|
||||
var result = await _sut.LoadAllPipelinesAsync(directory);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.Count.ShouldBe(1);
|
||||
result["unnamed"].Name.ShouldBe("unnamed");
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class ConnectionTestServiceTests
|
||||
{
|
||||
private readonly ConnectionTestService _sut;
|
||||
|
||||
public ConnectionTestServiceTests()
|
||||
{
|
||||
_sut = new ConnectionTestService();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_Oracle_ReturnsNotImplemented()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = "Data Source=oracle;User Id=user;Password=pass;";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.Oracle);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldContain("not implemented");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_Generic_ReturnsCannotTest()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = "SomeGenericConnectionString";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.Generic);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldContain("Cannot test generic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_UnknownProvider_ReturnsUnknownProviderError()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = "Data Source=test";
|
||||
var unknownProvider = (ConnectionProvider)999;
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, unknownProvider);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldContain("Unknown provider");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_SqlServer_WithInvalidConnectionString_ReturnsFailure()
|
||||
{
|
||||
// Arrange - Use an obviously invalid connection string that will fail fast
|
||||
var connectionString = "Server=nonexistent-server-that-does-not-exist-12345;Database=TestDb;Integrated Security=true;Connect Timeout=1;";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldNotBeNullOrEmpty();
|
||||
result.Duration.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_SqlServer_MeasuresDuration()
|
||||
{
|
||||
// Arrange - Use an invalid connection string that will fail but should still measure duration
|
||||
var connectionString = "Server=nonexistent-server-12345;Database=TestDb;Integrated Security=true;Connect Timeout=1;";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
|
||||
|
||||
// Assert
|
||||
result.Duration.ShouldNotBeNull();
|
||||
result.Duration!.Value.ShouldBeGreaterThan(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_SqlServer_WithCancellation_ReturnsCancelledResult()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = "Server=nonexistent-server-that-takes-forever-12345;Database=TestDb;Integrated Security=true;Connect Timeout=30;";
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel(); // Cancel immediately
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer, cts.Token);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldContain("cancelled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_SqlServer_WithMalformedConnectionString_ReturnsFailure()
|
||||
{
|
||||
// Arrange - Malformed connection string should be handled gracefully
|
||||
var connectionString = "not a valid connection string at all!!!";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_SqlServer_WithEmptyConnectionString_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = "";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Message.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class DiffServiceTests
|
||||
{
|
||||
private readonly DiffService _sut;
|
||||
|
||||
public DiffServiceTests()
|
||||
{
|
||||
_sut = new DiffService();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithNoChanges_ReturnsEmptyDiff()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2\nline3";
|
||||
var modified = "line1\nline2\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithChanges_ReturnsDiffLines()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2\nline3";
|
||||
var modified = "line1\nmodified\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Lines.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithAddedLine_ReportsInsertion()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2";
|
||||
var modified = "line1\nline2\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Insertions.ShouldBe(1);
|
||||
result.Deletions.ShouldBe(0);
|
||||
result.Lines.ShouldContain(l => l.Type == DiffLineType.Added && l.Text == "line3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithRemovedLine_ReportsDeletion()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2\nline3";
|
||||
var modified = "line1\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Deletions.ShouldBe(1);
|
||||
result.Lines.ShouldContain(l => l.Type == DiffLineType.Removed && l.Text == "line2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithModifiedLine_ReportsAdditionAndDeletion()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\noriginal\nline3";
|
||||
var modified = "line1\nchanged\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Insertions.ShouldBeGreaterThan(0);
|
||||
result.Deletions.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithEmptyOriginal_ReportsAllAsInsertions()
|
||||
{
|
||||
// Arrange
|
||||
var original = "";
|
||||
var modified = "line1\nline2";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Insertions.ShouldBeGreaterThan(0);
|
||||
result.Deletions.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_WithEmptyModified_ReportsAllAsDeletions()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2";
|
||||
var modified = "";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.Insertions.ShouldBe(0);
|
||||
result.Deletions.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_LineNumbers_AreCorrectForUnchangedLines()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2\nline3";
|
||||
var modified = "line1\nline2\nline3";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
var firstLine = result.Lines.First();
|
||||
firstLine.OldLineNumber.ShouldBe(1);
|
||||
firstLine.NewLineNumber.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_AddedLine_HasNullOldLineNumber()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1";
|
||||
var modified = "line1\nline2";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
var addedLine = result.Lines.FirstOrDefault(l => l.Type == DiffLineType.Added);
|
||||
addedLine.ShouldNotBeNull();
|
||||
addedLine.OldLineNumber.ShouldBeNull();
|
||||
addedLine.NewLineNumber.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDiff_RemovedLine_HasNullNewLineNumber()
|
||||
{
|
||||
// Arrange
|
||||
var original = "line1\nline2";
|
||||
var modified = "line1";
|
||||
|
||||
// Act
|
||||
var result = _sut.GenerateDiff(original, modified);
|
||||
|
||||
// Assert
|
||||
var removedLine = result.Lines.FirstOrDefault(l => l.Type == DiffLineType.Removed);
|
||||
removedLine.ShouldNotBeNull();
|
||||
removedLine.OldLineNumber.ShouldNotBeNull();
|
||||
removedLine.NewLineNumber.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class FileSystemTests
|
||||
{
|
||||
[Fact]
|
||||
public void FileExists_WithExistingFile_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new FileSystem();
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = sut.FileExists(tempFile);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileExists_WithNonExistingFile_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new FileSystem();
|
||||
|
||||
// Act
|
||||
var result = sut.FileExists("/nonexistent/path/file.txt");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class RuntimeConfigValidationServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _testDirectory;
|
||||
private readonly ISecureStoreManager _secureStoreManager;
|
||||
private readonly RuntimeConfigValidationService _sut;
|
||||
|
||||
public RuntimeConfigValidationServiceTests()
|
||||
{
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), $"RuntimeConfigValidationTests_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
|
||||
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
||||
_sut = new RuntimeConfigValidationService(_secureStoreManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
Directory.Delete(_testDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WithMissingAppSettings_ReturnsConfigurationError()
|
||||
{
|
||||
// Arrange - directory exists but no appsettings.json
|
||||
var configFolder = Path.Combine(_testDirectory, "missing");
|
||||
Directory.CreateDirectory(configFolder);
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(configFolder);
|
||||
|
||||
// Assert
|
||||
results.ShouldNotBeEmpty();
|
||||
var configResult = results.FirstOrDefault(r => r.ValidatorName == "Configuration");
|
||||
configResult.ShouldNotBeNull();
|
||||
configResult.IsValid.ShouldBeFalse();
|
||||
configResult.Errors.ShouldContain(e => e.Contains("appsettings.json not found"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WhenSecureStoreNotOpen_AddsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var configFolder = _testDirectory;
|
||||
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
_secureStoreManager.IsStoreOpen.Returns(false);
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(configFolder);
|
||||
|
||||
// Assert
|
||||
var storeResult = results.FirstOrDefault(r => r.ValidatorName == "SecureStore");
|
||||
storeResult.ShouldNotBeNull();
|
||||
storeResult.Warnings.ShouldContain(w => w.Contains("No SecureStore is currently open"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WhenSecureStoreIsOpen_NoSecureStoreWarning()
|
||||
{
|
||||
// Arrange
|
||||
var configFolder = _testDirectory;
|
||||
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(configFolder);
|
||||
|
||||
// Assert
|
||||
// There may be a SecureStore result from validators, but it should not have the
|
||||
// "No SecureStore is currently open" warning
|
||||
var storeResults = results.Where(r => r.ValidatorName == "SecureStore").ToList();
|
||||
foreach (var storeResult in storeResults)
|
||||
{
|
||||
storeResult.Warnings.ShouldNotContain(w => w.Contains("No SecureStore is currently open"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WithValidAppSettings_RunsValidators()
|
||||
{
|
||||
// Arrange
|
||||
var configFolder = _testDirectory;
|
||||
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
|
||||
var validConfig = """
|
||||
{
|
||||
"ConnectionStrings": {},
|
||||
"DataSync": {
|
||||
"Enabled": true
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(appSettingsPath, validConfig);
|
||||
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(configFolder);
|
||||
|
||||
// Assert
|
||||
// Should return results (empty or with validators) without throwing
|
||||
results.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WithNonExistentDirectory_ReturnsConfigurationError()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentFolder = Path.Combine(_testDirectory, "does-not-exist");
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(nonExistentFolder);
|
||||
|
||||
// Assert
|
||||
results.ShouldNotBeEmpty();
|
||||
var configResult = results.FirstOrDefault(r => r.ValidatorName == "Configuration");
|
||||
configResult.ShouldNotBeNull();
|
||||
configResult.IsValid.ShouldBeFalse();
|
||||
configResult.Errors.ShouldContain(e => e.Contains("not found"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_WithInvalidJson_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var configFolder = _testDirectory;
|
||||
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{ invalid json }");
|
||||
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
// The service should either throw or return an error result for invalid JSON
|
||||
Should.Throw<Exception>(() => _sut.ValidateRuntimeConfig(configFolder));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateRuntimeConfig_ReturnsMultipleResults_WhenMultipleValidatorsRun()
|
||||
{
|
||||
// Arrange
|
||||
var configFolder = _testDirectory;
|
||||
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
|
||||
var validConfig = """
|
||||
{
|
||||
"ConnectionStrings": {},
|
||||
"DataSync": {
|
||||
"Enabled": true
|
||||
},
|
||||
"SecureStore": {
|
||||
"StorePath": "data/secrets.json",
|
||||
"KeyFilePath": "data/secrets.key"
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(appSettingsPath, validConfig);
|
||||
|
||||
_secureStoreManager.IsStoreOpen.Returns(false);
|
||||
|
||||
// Act
|
||||
var results = _sut.ValidateRuntimeConfig(configFolder);
|
||||
|
||||
// Assert
|
||||
// Should have at least the SecureStore warning
|
||||
results.ShouldNotBeEmpty();
|
||||
}
|
||||
}
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
using System.IO;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services.SecureStore;
|
||||
|
||||
public class SecureStoreManagerTests : IDisposable
|
||||
{
|
||||
private readonly string _testDirectory;
|
||||
private readonly SecureStoreManager _sut;
|
||||
|
||||
public SecureStoreManagerTests()
|
||||
{
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), $"SecureStoreTests_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
_sut = new SecureStoreManager();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sut.Dispose();
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
Directory.Delete(_testDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsStoreOpen_WhenNoStoreOpen_ReturnsFalse()
|
||||
{
|
||||
_sut.IsStoreOpen.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentStorePath_WhenNoStoreOpen_ReturnsNull()
|
||||
{
|
||||
_sut.CurrentStorePath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasUnsavedChanges_WhenNoStoreOpen_ReturnsFalse()
|
||||
{
|
||||
_sut.HasUnsavedChanges.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateStore_WithKeyFile_CreatesStoreAndKeyFile()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
|
||||
// Act
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Assert
|
||||
_sut.IsStoreOpen.ShouldBeTrue();
|
||||
_sut.CurrentStorePath.ShouldBe(storePath);
|
||||
File.Exists(storePath).ShouldBeTrue();
|
||||
File.Exists(keyPath).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenStore_WithValidKeyFile_OpensStore()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.CloseStore();
|
||||
|
||||
// Act
|
||||
_sut.OpenStore(storePath, keyPath);
|
||||
|
||||
// Assert
|
||||
_sut.IsStoreOpen.ShouldBeTrue();
|
||||
_sut.CurrentStorePath.ShouldBe(storePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenStore_WithNonExistentStore_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "nonexistent.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<FileNotFoundException>(() => _sut.OpenStore(storePath, keyPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseStore_ClosesOpenStore()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act
|
||||
_sut.CloseStore();
|
||||
|
||||
// Assert
|
||||
_sut.IsStoreOpen.ShouldBeFalse();
|
||||
_sut.CurrentStorePath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetSecret_AddsSecretAndMarksUnsaved()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.Save(); // Save to clear unsaved flag
|
||||
|
||||
// Act
|
||||
_sut.SetSecret("testKey", "testValue");
|
||||
|
||||
// Assert
|
||||
_sut.HasUnsavedChanges.ShouldBeTrue();
|
||||
_sut.GetKeys().ShouldContain("testKey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSecret_ReturnsCorrectValue()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("testKey", "testValue");
|
||||
|
||||
// Act
|
||||
var value = _sut.GetSecret("testKey");
|
||||
|
||||
// Assert
|
||||
value.ShouldBe("testValue");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSecret_WhenKeyNotFound_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<KeyNotFoundException>(() => _sut.GetSecret("nonexistent"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveSecret_RemovesSecretAndMarksUnsaved()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("testKey", "testValue");
|
||||
_sut.Save();
|
||||
|
||||
// Act
|
||||
_sut.RemoveSecret("testKey");
|
||||
|
||||
// Assert
|
||||
_sut.HasUnsavedChanges.ShouldBeTrue();
|
||||
_sut.GetKeys().ShouldNotContain("testKey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveSecret_WhenKeyNotFound_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<KeyNotFoundException>(() => _sut.RemoveSecret("nonexistent"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_PersistsSecretsToStore()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("testKey", "testValue");
|
||||
|
||||
// Act
|
||||
_sut.Save();
|
||||
_sut.CloseStore();
|
||||
_sut.OpenStore(storePath, keyPath);
|
||||
|
||||
// Assert
|
||||
_sut.GetKeys().ShouldContain("testKey");
|
||||
_sut.GetSecret("testKey").ShouldBe("testValue");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_ClearsUnsavedChangesFlag()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("testKey", "testValue");
|
||||
_sut.HasUnsavedChanges.ShouldBeTrue();
|
||||
|
||||
// Act
|
||||
_sut.Save();
|
||||
|
||||
// Assert
|
||||
_sut.HasUnsavedChanges.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetKeys_ReturnsAllSecretKeys()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("key1", "value1");
|
||||
_sut.SetSecret("key2", "value2");
|
||||
_sut.SetSecret("key3", "value3");
|
||||
|
||||
// Act
|
||||
var keys = _sut.GetKeys();
|
||||
|
||||
// Assert
|
||||
keys.Count.ShouldBe(3);
|
||||
keys.ShouldContain("key1");
|
||||
keys.ShouldContain("key2");
|
||||
keys.ShouldContain("key3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKeyFile_CreatesNewKeyFile()
|
||||
{
|
||||
// Arrange
|
||||
var keyPath = Path.Combine(_testDirectory, "generated.key");
|
||||
|
||||
// Act
|
||||
_sut.GenerateKeyFile(keyPath);
|
||||
|
||||
// Assert
|
||||
File.Exists(keyPath).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExportKey_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var keyPath = Path.Combine(_testDirectory, "export.key");
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => _sut.ExportKey(keyPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExportKey_WhenStoreOpen_ExportsKey()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
var exportPath = Path.Combine(_testDirectory, "export.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act
|
||||
_sut.ExportKey(exportPath);
|
||||
|
||||
// Assert
|
||||
File.Exists(exportPath).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetKeys_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => _sut.GetKeys());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetSecret_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => _sut.SetSecret("key", "value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSecret_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => _sut.GetSecret("key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_AddsMissingKeys()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("existingKey", "existingValue");
|
||||
_sut.Save();
|
||||
|
||||
// Act
|
||||
var addedKeys = _sut.EnsureAllRequiredEntries(
|
||||
new[] { "existingKey", "newKey1" },
|
||||
new[] { "LotFinder", "JDE" });
|
||||
|
||||
// Assert
|
||||
addedKeys.Count.ShouldBe(3);
|
||||
addedKeys.ShouldContain("newKey1");
|
||||
addedKeys.ShouldContain("LotFinder");
|
||||
addedKeys.ShouldContain("JDE");
|
||||
addedKeys.ShouldNotContain("existingKey");
|
||||
|
||||
// Verify all keys exist
|
||||
_sut.GetKeys().ShouldContain("existingKey");
|
||||
_sut.GetKeys().ShouldContain("newKey1");
|
||||
_sut.GetKeys().ShouldContain("LotFinder");
|
||||
_sut.GetKeys().ShouldContain("JDE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_CreatesEmptyValues()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act
|
||||
_sut.EnsureAllRequiredEntries(
|
||||
new[] { "newKey" },
|
||||
Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
_sut.GetSecret("newKey").ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_DoesNotOverwriteExistingValues()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("existingKey", "originalValue");
|
||||
_sut.Save();
|
||||
|
||||
// Act
|
||||
var addedKeys = _sut.EnsureAllRequiredEntries(
|
||||
new[] { "existingKey" },
|
||||
Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
addedKeys.ShouldBeEmpty(); // Key already existed
|
||||
_sut.GetSecret("existingKey").ShouldBe("originalValue"); // Value preserved
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_AutoSavesWhenKeysAdded()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act
|
||||
_sut.EnsureAllRequiredEntries(
|
||||
new[] { "newKey" },
|
||||
Array.Empty<string>());
|
||||
|
||||
// Assert - should auto-save (HasUnsavedChanges should be false after calling EnsureAllRequiredEntries)
|
||||
_sut.HasUnsavedChanges.ShouldBeFalse();
|
||||
|
||||
// Verify persistence
|
||||
_sut.CloseStore();
|
||||
_sut.OpenStore(storePath, keyPath);
|
||||
_sut.GetKeys().ShouldContain("newKey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_ReturnsEmptyList_WhenNoMissingKeys()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
_sut.SetSecret("key1", "value1");
|
||||
_sut.SetSecret("key2", "value2");
|
||||
_sut.Save();
|
||||
|
||||
// Act
|
||||
var addedKeys = _sut.EnsureAllRequiredEntries(
|
||||
new[] { "key1" },
|
||||
new[] { "key2" });
|
||||
|
||||
// Assert
|
||||
addedKeys.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => _sut.EnsureAllRequiredEntries(
|
||||
new[] { "key" },
|
||||
Array.Empty<string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureAllRequiredEntries_HandlesDuplicatesAcrossLists()
|
||||
{
|
||||
// Arrange
|
||||
var storePath = Path.Combine(_testDirectory, "test.json");
|
||||
var keyPath = Path.Combine(_testDirectory, "test.key");
|
||||
_sut.CreateStore(storePath, keyPath);
|
||||
|
||||
// Act - same key in both lists should not cause issues
|
||||
var addedKeys = _sut.EnsureAllRequiredEntries(
|
||||
new[] { "sharedKey" },
|
||||
new[] { "sharedKey" });
|
||||
|
||||
// Assert - should only add once
|
||||
addedKeys.Count.ShouldBe(1);
|
||||
addedKeys.ShouldContain("sharedKey");
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
||||
|
||||
public class ValidationServiceTests
|
||||
{
|
||||
private readonly ValidationService _sut;
|
||||
|
||||
public ValidationServiceTests()
|
||||
{
|
||||
_sut = new ValidationService();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAppSettings_WithValidConfig_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel
|
||||
{
|
||||
DataSync = new DataSyncSection { MaxDegreeOfParallelism = 4 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidateAppSettings(config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeTrue();
|
||||
result.Errors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAppSettings_WithInvalidParallelism_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel
|
||||
{
|
||||
DataSync = new DataSyncSection { MaxDegreeOfParallelism = 0 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidateAppSettings(config);
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("MaxDegreeOfParallelism"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipelines_WithEmptyName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
[""] = new EtlPipelineConfig()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipelines(pipelines);
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipelines_WithInvalidConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["Test"] = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "invalid" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipelines(pipelines);
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("Connection"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_WithValidConfig_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Name = "Test",
|
||||
IsEnabled = true,
|
||||
Source = new SourceElement
|
||||
{
|
||||
Connection = "jde",
|
||||
Query = "SELECT * FROM Test"
|
||||
},
|
||||
Destination = new DestinationElement
|
||||
{
|
||||
Table = "TestTable",
|
||||
MatchColumns = ["Id"]
|
||||
},
|
||||
HourlySyncIntervalMinutes = 60
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeTrue();
|
||||
result.Errors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_WithMissingQuery_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "jde", Query = "" },
|
||||
Destination = new DestinationElement { Table = "TestTable" },
|
||||
IsManualOnly = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("Query"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_WithNoScheduleAndNotManual_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable", MatchColumns = ["Id"] },
|
||||
IsManualOnly = false // No schedules and not manual-only
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.Warnings.ShouldContain(w => w.Contains("No sync schedule"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_ManualOnly_DoesNotRequireSchedule()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable", MatchColumns = ["Id"] },
|
||||
IsManualOnly = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.Warnings.ShouldNotContain(w => w.Contains("No sync schedule"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_WithIntervalBelowMinimum_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable" },
|
||||
HourlySyncIntervalMinutes = 5 // Below minimum of 15
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain(e => e.Contains("Hourly sync interval"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePipeline_WithNoMatchColumns_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable", MatchColumns = [] },
|
||||
IsManualOnly = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.ValidatePipeline(pipeline, "Test");
|
||||
|
||||
// Assert
|
||||
result.Warnings.ShouldContain(w => w.Contains("No MatchColumns"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
global using Xunit;
|
||||
global using Shouldly;
|
||||
global using NSubstitute;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Utils\JdeScoping.ConfigManager.Ui\JdeScoping.ConfigManager.Ui.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Utils\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.*" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Headless;
|
||||
using JdeScoping.ConfigManager.Ui;
|
||||
|
||||
[assembly: AvaloniaTestApplication(typeof(JdeScoping.ConfigManager.Ui.Tests.TestAppBuilder))]
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a headless Avalonia application builder for UI tests.
|
||||
/// </summary>
|
||||
public class TestAppBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the Avalonia application configured for headless testing.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="AppBuilder"/> configured with headless platform options.</returns>
|
||||
public static AppBuilder BuildAvaloniaApp() =>
|
||||
AppBuilder.Configure<App>()
|
||||
.UseHeadless(new AvaloniaHeadlessPlatformOptions());
|
||||
}
|
||||
+241
@@ -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("");
|
||||
}
|
||||
}
|
||||
+181
@@ -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();
|
||||
}
|
||||
}
|
||||
+343
@@ -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);
|
||||
}
|
||||
}
|
||||
+170
@@ -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();
|
||||
}
|
||||
}
|
||||
+193
@@ -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");
|
||||
}
|
||||
}
|
||||
+91
@@ -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!));
|
||||
}
|
||||
}
|
||||
+219
@@ -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");
|
||||
}
|
||||
}
|
||||
+664
@@ -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");
|
||||
}
|
||||
}
|
||||
+43
@@ -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();
|
||||
}
|
||||
}
|
||||
+76
@@ -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();
|
||||
}
|
||||
}
|
||||
+149
@@ -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!));
|
||||
}
|
||||
}
|
||||
+60
@@ -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");
|
||||
}
|
||||
}
|
||||
+630
@@ -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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
+108
@@ -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);
|
||||
}
|
||||
}
|
||||
+412
@@ -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);
|
||||
}
|
||||
}
|
||||
+783
@@ -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);
|
||||
}
|
||||
}
|
||||
+756
@@ -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);
|
||||
}
|
||||
}
|
||||
+231
@@ -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);
|
||||
}
|
||||
}
|
||||
+283
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.VisualTree;
|
||||
using JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Tests.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// UI tests for dialog views.
|
||||
/// </summary>
|
||||
public class DialogViewTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog renders with the expected input fields.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_RendersInputFields()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert - NewStoreDialog contains TextBoxes for store path and key file path
|
||||
var textBoxes = dialog.GetVisualDescendants().OfType<TextBox>().ToList();
|
||||
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(2); // Store path and Key file path
|
||||
|
||||
// Verify section headers
|
||||
var textBlocks = dialog.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Store Location").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Key File").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog has Browse and Generate buttons for key file.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_HasBrowseAndGenerateButtons()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
|
||||
|
||||
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
|
||||
buttonContents.ShouldContain("Browse...");
|
||||
buttonContents.ShouldContain("Generate");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog has Create and Cancel buttons.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_HasCreateAndCancelButtons()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
|
||||
|
||||
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
|
||||
buttonContents.ShouldContain("Create");
|
||||
buttonContents.ShouldContain("Cancel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog has the correct title.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_HasCorrectTitle()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
dialog.Title.ShouldBe("Create New Secure Store");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog has the expected size constraints.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_HasExpectedSize()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert - Width=550, Height=350, MinWidth=450, MinHeight=300
|
||||
dialog.Width.ShouldBe(550);
|
||||
dialog.Height.ShouldBe(350);
|
||||
dialog.MinWidth.ShouldBe(450);
|
||||
dialog.MinHeight.ShouldBe(300);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog renders with key file input field.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_RendersKeyFileField()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert - UnlockStoreDialog contains TextBoxes for store path (read-only) and key file path
|
||||
var textBoxes = dialog.GetVisualDescendants().OfType<TextBox>().ToList();
|
||||
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(2); // Store path and Key file path
|
||||
|
||||
// Verify section headers
|
||||
var textBlocks = dialog.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Store File").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Key File").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog has a Browse button for key file.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_HasBrowseButton()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
|
||||
|
||||
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
|
||||
buttonContents.ShouldContain("Browse...");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog has Unlock and Cancel buttons.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_HasUnlockAndCancelButtons()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
|
||||
|
||||
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
|
||||
buttonContents.ShouldContain("Unlock");
|
||||
buttonContents.ShouldContain("Cancel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog has the correct title.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_HasCorrectTitle()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
dialog.Title.ShouldBe("Unlock Secure Store");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog has the expected size constraints.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_HasExpectedSize()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert - Width=500, Height=320, MinWidth=400, MinHeight=280
|
||||
dialog.Width.ShouldBe(500);
|
||||
dialog.Height.ShouldBe(320);
|
||||
dialog.MinWidth.ShouldBe(400);
|
||||
dialog.MinHeight.ShouldBe(280);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog is not resizable.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_IsNotResizable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
dialog.CanResize.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog is not resizable.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_IsNotResizable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
dialog.CanResize.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that NewStoreDialog contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void NewStoreDialogView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new NewStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var scrollViewer = dialog.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UnlockStoreDialog contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void UnlockStoreDialogView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var dialog = new UnlockStoreDialog();
|
||||
dialog.Show();
|
||||
|
||||
// Assert
|
||||
var scrollViewer = dialog.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.VisualTree;
|
||||
using JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Tests.Views.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// UI tests for form views.
|
||||
/// </summary>
|
||||
public class FormViewTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a test window to host a UserControl and shows it.
|
||||
/// UserControls must be attached to a Window to have their visual tree built.
|
||||
/// </summary>
|
||||
private static Window ShowInTestWindow(Control content)
|
||||
{
|
||||
var window = new Window { Content = content };
|
||||
window.Show();
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AuthFormView renders with the expected controls.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void AuthFormView_RendersWithCorrectControls()
|
||||
{
|
||||
// Arrange
|
||||
var view = new AuthFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert - AuthFormView contains TextBox for CookieName and NumericUpDown for expiration
|
||||
var textBoxes = view.GetVisualDescendants().OfType<TextBox>().ToList();
|
||||
textBoxes.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
|
||||
numericUpDowns.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
// Verify header is present
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Authentication Settings").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Cookie Settings").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DataSyncFormView renders with the expected controls.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void DataSyncFormView_RendersWithCorrectControls()
|
||||
{
|
||||
// Arrange
|
||||
var view = new DataSyncFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert - DataSyncFormView contains CheckBox for Enabled and multiple NumericUpDowns
|
||||
var checkBoxes = view.GetVisualDescendants().OfType<CheckBox>().ToList();
|
||||
checkBoxes.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
|
||||
numericUpDowns.Count.ShouldBeGreaterThanOrEqualTo(6); // Check Interval, Timeout, Max Parallelism, Batch Size, Bulk Copy Batch Size, Lookback Multiplier, Purge Retention
|
||||
|
||||
// Verify section headers
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Data Sync Settings").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Sync Intervals").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Performance").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Data Retention").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ConnectionStringsFormView renders with a DataGrid.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void ConnectionStringsFormView_RendersDataGrid()
|
||||
{
|
||||
// Arrange
|
||||
var view = new ConnectionStringsFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert - ConnectionStringsFormView contains a DataGrid for connections
|
||||
var dataGrid = view.FindDescendantOfType<DataGrid>();
|
||||
dataGrid.ShouldNotBeNull();
|
||||
|
||||
// Verify header is present
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Connection Strings").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Connections").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ConnectionStringsFormView has Add and Delete buttons.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void ConnectionStringsFormView_HasAddAndDeleteButtons()
|
||||
{
|
||||
// Arrange
|
||||
var view = new ConnectionStringsFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var buttons = view.GetVisualDescendants().OfType<Button>().ToList();
|
||||
|
||||
// Find buttons containing TextBlock with Add/Delete text
|
||||
var buttonTexts = buttons
|
||||
.SelectMany(b => b.GetVisualDescendants().OfType<TextBlock>())
|
||||
.Select(tb => tb.Text)
|
||||
.ToList();
|
||||
|
||||
buttonTexts.ShouldContain("Add");
|
||||
buttonTexts.ShouldContain("Delete");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that LdapFormView renders with server URL list input.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void LdapFormView_RendersServerList()
|
||||
{
|
||||
// Arrange
|
||||
var view = new LdapFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert - LdapFormView contains TextBoxes for server URLs, Group DN, Search Base
|
||||
var textBoxes = view.GetVisualDescendants().OfType<TextBox>().ToList();
|
||||
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(3); // Server URLs, Group DN, Search Base, Admin Bypass Users
|
||||
|
||||
// Verify section headers
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "LDAP Settings").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Server Configuration").ShouldBeTrue();
|
||||
textBlocks.Any(tb => tb.Text == "Directory Structure").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that LdapFormView contains a CheckBox for fake authentication.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void LdapFormView_HasFakeAuthCheckBox()
|
||||
{
|
||||
// Arrange
|
||||
var view = new LdapFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var checkBoxes = view.GetVisualDescendants().OfType<CheckBox>().ToList();
|
||||
checkBoxes.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
// Verify development options section
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Development Options").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that LdapFormView contains a NumericUpDown for connection timeout.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void LdapFormView_HasConnectionTimeoutNumericUpDown()
|
||||
{
|
||||
// Arrange
|
||||
var view = new LdapFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
|
||||
numericUpDowns.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
// Verify timeout label is present
|
||||
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
|
||||
textBlocks.Any(tb => tb.Text == "Connection Timeout (seconds)").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AuthFormView contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void AuthFormView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange
|
||||
var view = new AuthFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DataSyncFormView contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void DataSyncFormView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange
|
||||
var view = new DataSyncFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ConnectionStringsFormView contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void ConnectionStringsFormView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange
|
||||
var view = new ConnectionStringsFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that LdapFormView contains a ScrollViewer.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void LdapFormView_ContainsScrollViewer()
|
||||
{
|
||||
// Arrange
|
||||
var view = new LdapFormView();
|
||||
ShowInTestWindow(view);
|
||||
|
||||
// Assert
|
||||
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
|
||||
scrollViewer.ShouldNotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.VisualTree;
|
||||
using JdeScoping.ConfigManager.Ui.Views;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Tests.Views;
|
||||
|
||||
/// <summary>
|
||||
/// UI tests for <see cref="MainWindow"/>.
|
||||
/// </summary>
|
||||
public class MainWindowTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that the main window displays with the correct title.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ShowsWithCorrectTitle()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
window.Title.ShouldBe("JdeScoping ConfigManager");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window contains a TreeView for navigation.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsTreeView()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var treeView = window.FindDescendantOfType<TreeView>();
|
||||
treeView.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window contains a menu bar.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsMenuBar()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var menu = window.FindDescendantOfType<Menu>();
|
||||
menu.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window has the expected minimum size constraints.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_HasExpectedMinimumSize()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert - MinWidth=900, MinHeight=600 as defined in AXAML
|
||||
window.MinWidth.ShouldBe(900);
|
||||
window.MinHeight.ShouldBe(600);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window has the expected default size.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_HasExpectedDefaultSize()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert - Width=1200, Height=800 as defined in AXAML
|
||||
window.Width.ShouldBe(1200);
|
||||
window.Height.ShouldBe(800);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window contains toolbar buttons.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsToolbarButtons()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var buttons = window.GetVisualDescendants().OfType<Button>().ToList();
|
||||
buttons.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
// Verify toolbar buttons exist with expected content
|
||||
buttons.Any(b => b.Content?.ToString() == "Open").ShouldBeTrue();
|
||||
buttons.Any(b => b.Content?.ToString() == "Save").ShouldBeTrue();
|
||||
buttons.Any(b => b.Content?.ToString() == "Undo").ShouldBeTrue();
|
||||
buttons.Any(b => b.Content?.ToString() == "Redo").ShouldBeTrue();
|
||||
buttons.Any(b => b.Content?.ToString() == "Validate").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window contains a ContentControl for form display.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsFormContentControl()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var contentControl = window.FindDescendantOfType<ContentControl>();
|
||||
contentControl.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the main window contains a GridSplitter for resizing panels.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsGridSplitter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var splitter = window.FindDescendantOfType<GridSplitter>();
|
||||
splitter.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the menu contains expected menu items.
|
||||
/// </summary>
|
||||
[AvaloniaFact]
|
||||
public void MainWindow_ContainsExpectedMenuItems()
|
||||
{
|
||||
// Arrange & Act
|
||||
var window = new MainWindow();
|
||||
window.Show();
|
||||
|
||||
// Assert
|
||||
var menuItems = window.GetVisualDescendants().OfType<MenuItem>().ToList();
|
||||
menuItems.Count.ShouldBeGreaterThan(0);
|
||||
|
||||
// Verify top-level menu items exist
|
||||
menuItems.Any(m => m.Header?.ToString() == "_File").ShouldBeTrue();
|
||||
menuItems.Any(m => m.Header?.ToString() == "_Edit").ShouldBeTrue();
|
||||
menuItems.Any(m => m.Header?.ToString() == "_Tools").ShouldBeTrue();
|
||||
menuItems.Any(m => m.Header?.ToString() == "_Pipelines").ShouldBeTrue();
|
||||
menuItems.Any(m => m.Header?.ToString() == "_Secure Stores").ShouldBeTrue();
|
||||
menuItems.Any(m => m.Header?.ToString() == "_Help").ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user