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;
|
||||
+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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user