diff --git a/src/AdaptiveRemote.App/AppHostBuilderExtensions.cs b/src/AdaptiveRemote.App/AppHostBuilderExtensions.cs index 5f6b9ca4..7836e501 100644 --- a/src/AdaptiveRemote.App/AppHostBuilderExtensions.cs +++ b/src/AdaptiveRemote.App/AppHostBuilderExtensions.cs @@ -1,4 +1,5 @@ using AdaptiveRemote.Configuration; +using AdaptiveRemote.Services.Lifecycle; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -17,10 +18,13 @@ public static IHostBuilder ConfigureApp(this IHostBuilder hostBuilder) .AddSystemWrapperServices() .OptionallyAddTestHookEndpoint(); - public static IHostBuilder ConfigureAppSettings(this IHostBuilder hostBuilder, string[] args) + public static IHostBuilder ConfigureAppSettings(this IHostBuilder hostBuilder, AcceleratedServices acceleratedServices) => hostBuilder .ConfigureAppConfiguration(config => { + // Add startup configuration sources to maintain consistency between startup and host + config.AddConfiguration(acceleratedServices.StartupConfig); + config.AddInMemoryCollection(new Dictionary { // This makes the default behavior to publish telemetry when the full application @@ -31,7 +35,6 @@ public static IHostBuilder ConfigureAppSettings(this IHostBuilder hostBuilder, s // ["telemetry:Publish"] = "True" }); config.AddUserSecrets(); - config.AddCommandLine(args); }); // This class is used to locate the user secrets assembly for this project. diff --git a/src/AdaptiveRemote.App/AppHostRunner.cs b/src/AdaptiveRemote.App/AppHostRunner.cs index 8680e1cb..aca61be2 100644 --- a/src/AdaptiveRemote.App/AppHostRunner.cs +++ b/src/AdaptiveRemote.App/AppHostRunner.cs @@ -20,7 +20,7 @@ public async Task RunAsync(CancellationToken cancellationToken) { IHostBuilder hostBuilder = ConfigureHostBuilder() .ConfigureServices(acceleratedServices.AddPrecreatedServices) - .ConfigureAppSettings(CommandLineArguments) + .ConfigureAppSettings(acceleratedServices) .ConfigureApp(); // Allow tests to inject services before the host is built diff --git a/src/AdaptiveRemote.App/Services/Lifecycle/AcceleratedServices.cs b/src/AdaptiveRemote.App/Services/Lifecycle/AcceleratedServices.cs index 1a85461f..34fbc809 100644 --- a/src/AdaptiveRemote.App/Services/Lifecycle/AcceleratedServices.cs +++ b/src/AdaptiveRemote.App/Services/Lifecycle/AcceleratedServices.cs @@ -1,4 +1,6 @@ -using AdaptiveRemote.Models; +using AdaptiveRemote.Configuration; +using AdaptiveRemote.Logging; +using AdaptiveRemote.Models; using AdaptiveRemote.Services.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -14,10 +16,15 @@ public class AcceleratedServices internal ITestEndpointHooks TestEndpoint { get; } /// - /// Command line configuration parsed from arguments. - /// Available for any startup services that need command line settings. + /// Raw command-line arguments passed to the application. /// - public IConfigurationRoot CommandLineConfig { get; } + internal string[] Args { get; } + + /// + /// Startup configuration built from appsettings.json, environment variables, and command-line args. + /// Available for any startup services that need settings before the host is configured. + /// + public IConfigurationRoot StartupConfig { get; } /// /// Logger factory for startup processes. @@ -27,31 +34,46 @@ public class AcceleratedServices public AcceleratedServices(string[] args) { - // Parse command line configuration early + Args = args; + + // Build startup configuration from appsettings.json, environment variables, and command line args + string environmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; + string basePath = AppContext.BaseDirectory; + ConfigurationBuilder configBuilder = new(); - CommandLineConfig = configBuilder - .AddCommandLine(args) - .Build(); + configBuilder.SetBasePath(basePath); + configBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); + configBuilder.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false); + configBuilder.AddEnvironmentVariables(); + configBuilder.AddCommandLine(args); + StartupConfig = configBuilder.Build(); + + // Create logger factory for startup processes, configured from StartupConfig + LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.AddConfiguration(StartupConfig.GetSection("Logging")); - // Create logger factory for startup processes - LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole()); + // Add file logging if configured + LoggingSettings loggingSettings = StartupConfig.GetSection(SettingsKeys.Logging).Get() + ?? new LoggingSettings(); + if (loggingSettings.FilePath is not null) + { + builder.AddProvider(new FileLoggerProvider(loggingSettings.FilePath)); + } + }); ViewModel = new(); Controller = new LifecycleViewController(ViewModel); DiagnosticAdapter = new(Controller); // Check if test control port is configured - int? controlPort = ParseControlPort(); - if (controlPort.HasValue) + TestingSettings testingSettings = StartupConfig.GetSection(SettingsKeys.Testing).Get() + ?? new TestingSettings(); + if (testingSettings.ControlPort.HasValue) { - // Create TestingSettings from command line config - TestingSettings testSettings = new() - { - ControlPort = controlPort - }; - // Create and start the test endpoint service - TestEndpointService testEndpointService = new(testSettings, LoggerFactory); + TestEndpointService testEndpointService = new(testingSettings, LoggerFactory); testEndpointService.StartListening(); TestEndpoint = testEndpointService; } @@ -69,15 +91,4 @@ public virtual void AddPrecreatedServices(IServiceCollection services) .AddSingleton(Controller) .AddSingleton(ViewModel); } - - private int? ParseControlPort() - { - string? portString = CommandLineConfig["test:ControlPort"]; - if (int.TryParse(portString, out int port)) - { - return port; - } - - return null; - } } diff --git a/src/AdaptiveRemote/Services/Lifecycle/WpfAcceleratedServices.cs b/src/AdaptiveRemote/Services/Lifecycle/WpfAcceleratedServices.cs index 5dc3ff22..51dd572e 100644 --- a/src/AdaptiveRemote/Services/Lifecycle/WpfAcceleratedServices.cs +++ b/src/AdaptiveRemote/Services/Lifecycle/WpfAcceleratedServices.cs @@ -13,9 +13,7 @@ public WpfAcceleratedServices(string[] args) { MainWindow = new(ViewModel); - TestingSettings? settings = new ConfigurationBuilder() - .AddCommandLine(args) - .Build() + TestingSettings? settings = StartupConfig .GetSection("test") .Get(); diff --git a/test/AdaptiveRemote.App.Tests/Services/Lifecycle/AcceleratedServicesTests.cs b/test/AdaptiveRemote.App.Tests/Services/Lifecycle/AcceleratedServicesTests.cs new file mode 100644 index 00000000..e4d9aba9 --- /dev/null +++ b/test/AdaptiveRemote.App.Tests/Services/Lifecycle/AcceleratedServicesTests.cs @@ -0,0 +1,161 @@ +using AdaptiveRemote; +using FluentAssertions; +using Microsoft.Extensions.Configuration; + +namespace AdaptiveRemote.Services.Lifecycle; + +[TestClass] +public class AcceleratedServicesTests +{ + [TestMethod] + public void AcceleratedServices_Constructor_InitializesArgs() + { + // Arrange + string[] args = ["--foo=bar"]; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.Args.Should().BeSameAs(args); + } + + [TestMethod] + public void AcceleratedServices_Constructor_BuildsStartupConfigFromCommandLineArgs() + { + // Arrange + string[] args = ["--custom=value"]; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.StartupConfig["custom"].Should().Be("value"); + } + + [TestMethod] + public void AcceleratedServices_StartupConfig_CommandLineArgsOverrideEnvironmentVariables() + { + // Arrange + string envVarName = "ACCELERATED_SERVICES_TEST_VAR"; + Environment.SetEnvironmentVariable(envVarName, "env-value"); + try + { + string[] args = [$"--{envVarName}=cli-value"]; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.StartupConfig[envVarName].Should().Be("cli-value"); + } + finally + { + Environment.SetEnvironmentVariable(envVarName, null); + } + } + + [TestMethod] + public void AcceleratedServices_StartupConfig_IncludesEnvironmentVariables() + { + // Arrange + string envVarName = "ACCELERATED_SERVICES_TEST_ENV"; + Environment.SetEnvironmentVariable(envVarName, "env-value"); + try + { + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.StartupConfig[envVarName].Should().Be("env-value"); + } + finally + { + Environment.SetEnvironmentVariable(envVarName, null); + } + } + + [TestMethod] + public void AcceleratedServices_LoggerFactory_IsConfigured() + { + // Arrange + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.LoggerFactory.Should().NotBeNull(); + } + + [TestMethod] + public void AcceleratedServices_ViewModel_IsInitialized() + { + // Arrange + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.ViewModel.Should().NotBeNull(); + sut.ViewModel.CurrentPhase.Should().Be(LifecyclePhase.Waiting); + } + + [TestMethod] + public void AcceleratedServices_Controller_IsInitialized() + { + // Arrange + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.Controller.Should().NotBeNull(); + } + + [TestMethod] + public void AcceleratedServices_DiagnosticAdapter_IsInitialized() + { + // Arrange + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.DiagnosticAdapter.Should().NotBeNull(); + } + + [TestMethod] + public void AcceleratedServices_TestEndpoint_StartsWhenControlPortConfigured() + { + // Arrange + int port = Random.Shared.Next(10000, 60000); + string[] args = [$"--test:ControlPort={port}"]; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.TestEndpoint.Should().NotBeNull(); + sut.TestEndpoint.Should().BeOfType(); + } + + [TestMethod] + public void AcceleratedServices_TestEndpoint_DisabledWhenControlPortNotConfigured() + { + // Arrange + string[] args = []; + + // Act + AcceleratedServices sut = new(args); + + // Assert + sut.TestEndpoint.Should().NotBeNull(); + sut.TestEndpoint.Should().BeOfType(); + } +}