ScadaLinkDbContext with 10 configuration classes (Fluent API only), initial migration creating 25 tables, environment-aware migration helper (auto-apply dev, validate-only prod), DesignTimeDbContextFactory, optimistic concurrency on DeploymentRecord. 20 tests verify schema, CRUD, relationships, cascades.
202 lines
7.6 KiB
C#
202 lines
7.6 KiB
C#
using System.Reflection;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using ScadaLink.ClusterInfrastructure;
|
|
using ScadaLink.Communication;
|
|
using ScadaLink.DataConnectionLayer;
|
|
using ScadaLink.ExternalSystemGateway;
|
|
using ScadaLink.HealthMonitoring;
|
|
using ScadaLink.Host;
|
|
using ScadaLink.NotificationService;
|
|
using ScadaLink.SiteEventLogging;
|
|
using ScadaLink.SiteRuntime;
|
|
using ScadaLink.StoreAndForward;
|
|
|
|
namespace ScadaLink.Host.Tests;
|
|
|
|
public class HostStartupTests : IDisposable
|
|
{
|
|
private readonly List<IDisposable> _disposables = new();
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var d in _disposables)
|
|
{
|
|
try { d.Dispose(); } catch { /* best effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CentralRole_StartsWithoutError()
|
|
{
|
|
// WebApplicationFactory replays Program.Main, which reads config from files.
|
|
// Set the environment to Central so appsettings.Central.json is loaded,
|
|
// and set DOTNET_ENVIRONMENT before the factory creates the host.
|
|
var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
|
|
try
|
|
{
|
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central");
|
|
|
|
var factory = new WebApplicationFactory<Program>()
|
|
.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("ScadaLink:Node:Role", "Central");
|
|
builder.UseSetting("ScadaLink:Database:SkipMigrations", "true");
|
|
});
|
|
_disposables.Add(factory);
|
|
|
|
// Creating the server exercises the full DI container build and startup pipeline
|
|
var client = factory.CreateClient();
|
|
_disposables.Add(client);
|
|
|
|
// If we get here without exception, the central host started successfully
|
|
Assert.NotNull(client);
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SiteRole_StartsWithoutError()
|
|
{
|
|
var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder();
|
|
builder.ConfigureAppConfiguration(config =>
|
|
{
|
|
config.Sources.Clear();
|
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaLink:Node:Role"] = "Site",
|
|
["ScadaLink:Node:NodeHostname"] = "test-site",
|
|
["ScadaLink:Node:SiteId"] = "TestSite",
|
|
["ScadaLink:Node:RemotingPort"] = "8082",
|
|
});
|
|
});
|
|
builder.ConfigureServices((context, services) =>
|
|
{
|
|
// Shared components
|
|
services.AddClusterInfrastructure();
|
|
services.AddCommunication();
|
|
services.AddHealthMonitoring();
|
|
services.AddExternalSystemGateway();
|
|
services.AddNotificationService();
|
|
|
|
// Site-only components
|
|
services.AddSiteRuntime();
|
|
services.AddDataConnectionLayer();
|
|
services.AddStoreAndForward();
|
|
services.AddSiteEventLogging();
|
|
|
|
// Options binding (mirrors Program.cs site path)
|
|
services.Configure<NodeOptions>(context.Configuration.GetSection("ScadaLink:Node"));
|
|
services.Configure<ClusterOptions>(context.Configuration.GetSection("ScadaLink:Cluster"));
|
|
services.Configure<DatabaseOptions>(context.Configuration.GetSection("ScadaLink:Database"));
|
|
services.Configure<CommunicationOptions>(context.Configuration.GetSection("ScadaLink:Communication"));
|
|
services.Configure<HealthMonitoringOptions>(context.Configuration.GetSection("ScadaLink:HealthMonitoring"));
|
|
services.Configure<NotificationOptions>(context.Configuration.GetSection("ScadaLink:Notification"));
|
|
services.Configure<LoggingOptions>(context.Configuration.GetSection("ScadaLink:Logging"));
|
|
services.Configure<DataConnectionOptions>(context.Configuration.GetSection("ScadaLink:DataConnection"));
|
|
services.Configure<StoreAndForwardOptions>(context.Configuration.GetSection("ScadaLink:StoreAndForward"));
|
|
services.Configure<SiteEventLogOptions>(context.Configuration.GetSection("ScadaLink:SiteEventLog"));
|
|
});
|
|
|
|
var host = builder.Build();
|
|
_disposables.Add(host);
|
|
|
|
// Build succeeds = DI container is valid and all services resolve
|
|
Assert.NotNull(host);
|
|
Assert.NotNull(host.Services);
|
|
}
|
|
|
|
[Fact]
|
|
public void SiteRole_DoesNotConfigureKestrel()
|
|
{
|
|
var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder();
|
|
builder.ConfigureAppConfiguration(config =>
|
|
{
|
|
config.Sources.Clear();
|
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaLink:Node:Role"] = "Site",
|
|
["ScadaLink:Node:NodeHostname"] = "test-site",
|
|
["ScadaLink:Node:SiteId"] = "TestSite",
|
|
["ScadaLink:Node:RemotingPort"] = "8082",
|
|
});
|
|
});
|
|
builder.ConfigureServices((context, services) =>
|
|
{
|
|
services.AddClusterInfrastructure();
|
|
services.AddCommunication();
|
|
services.AddHealthMonitoring();
|
|
services.AddExternalSystemGateway();
|
|
services.AddNotificationService();
|
|
services.AddSiteRuntime();
|
|
services.AddDataConnectionLayer();
|
|
services.AddStoreAndForward();
|
|
services.AddSiteEventLogging();
|
|
});
|
|
|
|
var host = builder.Build();
|
|
|
|
// Verify no Kestrel server or web host is registered.
|
|
// Host.CreateDefaultBuilder does not add Kestrel, so there should be no IServer.
|
|
var serverType = Type.GetType(
|
|
"Microsoft.AspNetCore.Hosting.Server.IServer, Microsoft.AspNetCore.Hosting.Server.Abstractions");
|
|
|
|
if (serverType != null)
|
|
{
|
|
var server = host.Services.GetService(serverType);
|
|
Assert.Null(server);
|
|
}
|
|
|
|
// Additionally verify no HTTP URLs are configured
|
|
var config = host.Services.GetRequiredService<IConfiguration>();
|
|
var urls = config["urls"] ?? config["ASPNETCORE_URLS"];
|
|
Assert.Null(urls);
|
|
|
|
host.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public void HostProject_DoesNotUseConditionalCompilation()
|
|
{
|
|
var hostProjectDir = FindHostProjectDirectory();
|
|
Assert.NotNull(hostProjectDir);
|
|
|
|
var sourceFiles = Directory.GetFiles(hostProjectDir, "*.cs", SearchOption.TopDirectoryOnly);
|
|
Assert.NotEmpty(sourceFiles);
|
|
|
|
foreach (var file in sourceFiles)
|
|
{
|
|
var content = File.ReadAllText(file);
|
|
|
|
Assert.DoesNotContain("#if", content);
|
|
Assert.DoesNotContain("#ifdef", content);
|
|
Assert.DoesNotContain("#ifndef", content);
|
|
Assert.DoesNotContain("#elif", content);
|
|
Assert.DoesNotContain("#else", content);
|
|
Assert.DoesNotContain("#endif", content);
|
|
}
|
|
}
|
|
|
|
private static string? FindHostProjectDirectory()
|
|
{
|
|
// Walk up from the test assembly location to find the src directory
|
|
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
|
var dir = new DirectoryInfo(assemblyDir);
|
|
|
|
while (dir != null)
|
|
{
|
|
var hostPath = Path.Combine(dir.FullName, "src", "ScadaLink.Host");
|
|
if (Directory.Exists(hostPath))
|
|
return hostPath;
|
|
dir = dir.Parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|