using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ScadaLink.Host; namespace ScadaLink.Host.Tests; public class HostStartupTests : IDisposable { private readonly List _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() .WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((_, config) => { config.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:NodeHostname"] = "localhost", ["ScadaLink:Node:RemotingPort"] = "0", ["ScadaLink:Cluster:SeedNodes:0"] = "akka.tcp://scadalink@localhost:2551", ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:2552", ["ScadaLink:Database:SkipMigrations"] = "true", }); }); 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 = WebApplication.CreateBuilder(); builder.Configuration.Sources.Clear(); builder.Configuration.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:Role"] = "Site", ["ScadaLink:Node:NodeHostname"] = "test-site", ["ScadaLink:Node:SiteId"] = "TestSite", ["ScadaLink:Node:RemotingPort"] = "0", ["ScadaLink:Node:GrpcPort"] = "0", }); builder.Services.AddGrpc(); builder.Services.AddSingleton(); SiteServiceRegistration.Configure(builder.Services, builder.Configuration); // Remove AkkaHostedService from running AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(builder.Services); var app = builder.Build(); _disposables.Add(app); // Build succeeds = DI container is valid and all services resolve Assert.NotNull(app); Assert.NotNull(app.Services); } [Fact] public void SiteRole_ConfiguresKestrelForGrpc() { var builder = WebApplication.CreateBuilder(); builder.Configuration.Sources.Clear(); builder.Configuration.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:Role"] = "Site", ["ScadaLink:Node:NodeHostname"] = "test-site", ["ScadaLink:Node:SiteId"] = "TestSite", ["ScadaLink:Node:RemotingPort"] = "0", ["ScadaLink:Node:GrpcPort"] = "0", }); builder.WebHost.ConfigureKestrel(options => { options.ListenAnyIP(0, listenOptions => { listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2; }); }); builder.Services.AddGrpc(); builder.Services.AddSingleton(); SiteServiceRegistration.Configure(builder.Services, builder.Configuration); // Remove AkkaHostedService from running AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(builder.Services); var app = builder.Build(); // Verify Kestrel IS configured (site now hosts gRPC via WebApplicationBuilder) var serverType = Type.GetType( "Microsoft.AspNetCore.Hosting.Server.IServer, Microsoft.AspNetCore.Hosting.Server.Abstractions"); if (serverType != null) { var server = app.Services.GetService(serverType); Assert.NotNull(server); } (app as IDisposable)?.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; } }