using FluentAssertions; using StellaOps.Cli.Commands.Setup.Steps; using StellaOps.Cli.Commands.Setup.Steps.Implementations; using StellaOps.Doctor.Detection; using Xunit; namespace StellaOps.Cli.Commands.Setup.Tests.Steps; [Trait("Category", "Unit")] public sealed class SetupStepImplementationsTests { [Fact] public void DatabaseSetupStep_HasCorrectMetadata() { // Arrange var step = new DatabaseSetupStep(); // Assert step.Id.Should().Be("database"); step.Name.Should().Be("PostgreSQL Database"); step.Category.Should().Be(SetupCategory.Infrastructure); step.IsRequired.Should().BeTrue(); step.IsSkippable.Should().BeFalse(); step.Order.Should().Be(10); step.Dependencies.Should().BeEmpty(); step.ValidationChecks.Should().Contain("check.database.connectivity"); } [Fact] public void CacheSetupStep_HasCorrectMetadata() { // Arrange var step = new CacheSetupStep(); // Assert step.Id.Should().Be("cache"); step.Name.Should().Be("Valkey/Redis Cache"); step.Category.Should().Be(SetupCategory.Infrastructure); step.IsRequired.Should().BeTrue(); step.Dependencies.Should().Contain("database"); step.Order.Should().Be(20); } [Fact] public void VaultSetupStep_HasCorrectMetadata() { // Arrange var step = new VaultSetupStep(); // Assert step.Id.Should().Be("vault"); step.Name.Should().Be("Secrets Vault"); step.Category.Should().Be(SetupCategory.Security); step.IsRequired.Should().BeFalse(); step.IsSkippable.Should().BeTrue(); step.ValidationChecks.Should().Contain("check.integration.vault.connectivity"); } [Fact] public void SettingsStoreSetupStep_HasCorrectMetadata() { // Arrange var step = new SettingsStoreSetupStep(); // Assert step.Id.Should().Be("settingsstore"); step.Name.Should().Be("Settings Store"); step.Category.Should().Be(SetupCategory.Configuration); step.IsRequired.Should().BeFalse(); step.IsSkippable.Should().BeTrue(); step.ValidationChecks.Should().Contain("check.integration.settingsstore.connectivity"); } [Fact] public void RegistrySetupStep_HasCorrectMetadata() { // Arrange var step = new RegistrySetupStep(); // Assert step.Id.Should().Be("registry"); step.Name.Should().Be("Container Registry"); step.Category.Should().Be(SetupCategory.Integration); step.IsRequired.Should().BeFalse(); step.ValidationChecks.Should().Contain("check.integration.registry.connectivity"); } [Fact] public void TelemetrySetupStep_HasCorrectMetadata() { // Arrange var step = new TelemetrySetupStep(); // Assert step.Id.Should().Be("telemetry"); step.Name.Should().Be("OpenTelemetry"); step.Category.Should().Be(SetupCategory.Observability); step.IsRequired.Should().BeFalse(); step.ValidationChecks.Should().Contain("check.telemetry.otlp.connectivity"); } [Fact] public async Task DatabaseSetupStep_CheckPrerequisites_Passes_WhenInteractive() { // Arrange var step = new DatabaseSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = false }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert result.Met.Should().BeTrue(); } [Fact] public async Task DatabaseSetupStep_CheckPrerequisites_Fails_WhenNonInteractiveWithoutConfig() { // Arrange var step = new DatabaseSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert result.Met.Should().BeFalse(); result.MissingPrerequisites.Should().Contain(s => s.Contains("database")); } [Fact] public async Task DatabaseSetupStep_CheckPrerequisites_Passes_WhenNonInteractiveWithConnectionString() { // Arrange var step = new DatabaseSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true, ConfigValues = new Dictionary { ["database.connectionString"] = "Host=localhost;Database=test" } }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert result.Met.Should().BeTrue(); } [Fact] public async Task DatabaseSetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new DatabaseSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["database.host"] = "localhost", ["database.port"] = "5432", ["database.database"] = "testdb", ["database.user"] = "testuser", ["database.password"] = "testpass" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig.Should().ContainKey("database.host"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task CacheSetupStep_CheckPrerequisites_Fails_WhenNonInteractiveWithoutConfig() { // Arrange var step = new CacheSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert result.Met.Should().BeFalse(); } [Fact] public async Task CacheSetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new CacheSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["cache.host"] = "localhost", ["cache.port"] = "6379" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig.Should().ContainKey("cache.host"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task SettingsStoreSetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected() { // Arrange var step = new SettingsStoreSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true // No provider in config, non-interactive mode }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Skipped); } [Fact] public async Task SettingsStoreSetupStep_Execute_DryRun_ConfiguresConsul() { // Arrange var step = new SettingsStoreSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["settingsstore.provider"] = "consul", ["settingsstore.address"] = "http://localhost:8500", ["settingsstore.prefix"] = "stellaops/" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["settingsstore.provider"].Should().Be("consul"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task VaultSetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected() { // Arrange var step = new VaultSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Skipped); } [Fact] public async Task TelemetrySetupStep_Execute_ReturnsSkipped_WhenNoEndpointProvided() { // Arrange var step = new TelemetrySetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Skipped); } [Fact] public async Task TelemetrySetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new TelemetrySetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["telemetry.otlpEndpoint"] = "http://localhost:4317", ["telemetry.serviceName"] = "test-service" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["telemetry.otlpEndpoint"].Should().Be("http://localhost:4317"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task RegistrySetupStep_Execute_ReturnsSkipped_WhenNoUrlProvided() { // Arrange var step = new RegistrySetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Skipped); } [Fact] public async Task RegistrySetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new RegistrySetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["registry.url"] = "https://registry.example.com", ["registry.username"] = "admin" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["registry.url"].Should().Be("https://registry.example.com"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public void AllSteps_HaveUniqueIds() { // Arrange var steps = new ISetupStep[] { new DatabaseSetupStep(), new CacheSetupStep(), new VaultSetupStep(), new SettingsStoreSetupStep(), new RegistrySetupStep(), new TelemetrySetupStep() }; // Assert var ids = steps.Select(s => s.Id).ToList(); ids.Should().OnlyHaveUniqueItems(); } [Fact] public void AllSteps_CanBeAddedToCatalog() { // Arrange var catalog = new SetupStepCatalog(); var steps = new ISetupStep[] { new DatabaseSetupStep(), new CacheSetupStep(), new VaultSetupStep(), new SettingsStoreSetupStep(), new RegistrySetupStep(), new TelemetrySetupStep() }; // Act foreach (var step in steps) { catalog.Register(step); } // Assert catalog.AllSteps.Should().HaveCount(6); } [Fact] public void StepCatalog_ResolvesExecutionOrder_WithDependencies() { // Arrange var catalog = new SetupStepCatalog(); catalog.Register(new DatabaseSetupStep()); catalog.Register(new CacheSetupStep()); catalog.Register(new VaultSetupStep()); catalog.Register(new SettingsStoreSetupStep()); // Act var orderedSteps = catalog.ResolveExecutionOrder().ToList(); // Assert orderedSteps.Should().HaveCount(4); // Database must come before Cache (Cache depends on Database) var databaseIndex = orderedSteps.FindIndex(s => s.Id == "database"); var cacheIndex = orderedSteps.FindIndex(s => s.Id == "cache"); databaseIndex.Should().BeLessThan(cacheIndex); } // ===================================== // Sprint 7-9 Setup Steps Tests // ===================================== [Fact] public void AuthoritySetupStep_HasCorrectMetadata() { // Arrange var step = new AuthoritySetupStep(); // Assert step.Id.Should().Be("authority"); step.Name.Should().Be("Authentication Provider"); step.Category.Should().Be(SetupCategory.Security); step.IsRequired.Should().BeTrue(); step.IsSkippable.Should().BeFalse(); step.Order.Should().Be(1); step.ValidationChecks.Should().Contain("check.authority.plugin.configured"); } [Fact] public async Task AuthoritySetupStep_CheckPrerequisites_Passes_WhenInteractive() { // Arrange var step = new AuthoritySetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = false }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert result.Met.Should().BeTrue(); } [Fact] public async Task AuthoritySetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new AuthoritySetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["authority.provider"] = "standard", ["authority.standard.passwordPolicy.minLength"] = "12" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig.Should().ContainKey("authority.provider"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public void UsersSetupStep_HasCorrectMetadata() { // Arrange var step = new UsersSetupStep(); // Assert step.Id.Should().Be("users"); step.Name.Should().Be("User Management"); step.Category.Should().Be(SetupCategory.Security); step.IsRequired.Should().BeTrue(); step.IsSkippable.Should().BeFalse(); step.Order.Should().Be(2); step.Dependencies.Should().Contain("authority"); step.ValidationChecks.Should().Contain("check.users.superuser.exists"); } [Fact] public async Task UsersSetupStep_Execute_DryRun_ReturnsSuccess() { // Arrange var step = new UsersSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["users.superuser.username"] = "admin", ["users.superuser.email"] = "admin@example.com", ["users.superuser.password"] = "SecurePass123!" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig.Should().ContainKey("users.superuser.username"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public void NotifySetupStep_HasCorrectMetadata() { // Arrange var step = new NotifySetupStep(); // Assert step.Id.Should().Be("notify"); step.Name.Should().Be("Notifications"); step.Category.Should().Be(SetupCategory.Integration); step.IsRequired.Should().BeFalse(); step.IsSkippable.Should().BeTrue(); step.Order.Should().Be(70); step.ValidationChecks.Should().Contain("check.notify.channel.configured"); } [Fact] public async Task NotifySetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected() { // Arrange var step = new NotifySetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Skipped); } [Fact] public async Task NotifySetupStep_Execute_DryRun_ConfiguresEmail() { // Arrange var step = new NotifySetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["notify.provider"] = "email", ["notify.email.smtpHost"] = "smtp.example.com", ["notify.email.smtpPort"] = "587", ["notify.email.fromAddress"] = "noreply@example.com" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["notify.provider"].Should().Be("email"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public void LlmSetupStep_HasCorrectMetadata() { // Arrange var step = new LlmSetupStep(); // Assert step.Id.Should().Be("llm"); step.Name.Should().Be("AI/LLM Provider"); step.Category.Should().Be(SetupCategory.Integration); step.IsRequired.Should().BeFalse(); step.IsSkippable.Should().BeTrue(); step.Order.Should().Be(80); step.ValidationChecks.Should().Contain("check.ai.llm.config"); step.ValidationChecks.Should().Contain("check.ai.provider.openai"); step.ValidationChecks.Should().Contain("check.ai.provider.claude"); step.ValidationChecks.Should().Contain("check.ai.provider.gemini"); } [Fact] public async Task LlmSetupStep_CheckPrerequisites_AlwaysPasses() { // Arrange var step = new LlmSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = true }; // Act var result = await step.CheckPrerequisitesAsync(context); // Assert - LLM setup has no prerequisites result.Met.Should().BeTrue(); } [Fact] public async Task LlmSetupStep_Execute_ReturnsSuccess_WhenNoneSelected() { // Arrange var step = new LlmSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, NonInteractive = false, ConfigValues = new Dictionary { ["llm.provider"] = "none" }, Output = msg => output.Add(msg), PromptForChoice = (prompt, options, defaultVal) => "none" }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig.Should().ContainKey("AdvisoryAI:Enabled"); result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("false"); } [Fact] public async Task LlmSetupStep_Execute_DryRun_ConfiguresOpenAi() { // Arrange var step = new LlmSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["llm.provider"] = "openai", ["llm.openai.apiKey"] = "sk-test-key-12345", ["llm.openai.model"] = "gpt-4o" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true"); result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("openai"); result.AppliedConfig["AdvisoryAI:LlmProviders:OpenAI:Model"].Should().Be("gpt-4o"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task LlmSetupStep_Execute_DryRun_ConfiguresClaude() { // Arrange var step = new LlmSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["llm.provider"] = "claude", ["llm.claude.apiKey"] = "sk-ant-test-key-12345", ["llm.claude.model"] = "claude-sonnet-4-20250514" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true"); result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("claude"); result.AppliedConfig["AdvisoryAI:LlmProviders:Claude:Model"].Should().Be("claude-sonnet-4-20250514"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task LlmSetupStep_Execute_DryRun_ConfiguresGemini() { // Arrange var step = new LlmSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["llm.provider"] = "gemini", ["llm.gemini.apiKey"] = "AIzaSy-test-key-12345", ["llm.gemini.model"] = "gemini-1.5-flash" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true"); result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("gemini"); result.AppliedConfig["AdvisoryAI:LlmProviders:Gemini:Model"].Should().Be("gemini-1.5-flash"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task LlmSetupStep_Execute_DryRun_ConfiguresOllama() { // Arrange var step = new LlmSetupStep(); var output = new List(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, DryRun = true, ConfigValues = new Dictionary { ["llm.provider"] = "ollama", ["llm.ollama.endpoint"] = "http://localhost:11434", ["llm.ollama.model"] = "llama3:8b" }, Output = msg => output.Add(msg) }; // Act var result = await step.ExecuteAsync(context); // Assert result.Status.Should().Be(SetupStepStatus.Completed); result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true"); result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("ollama"); result.AppliedConfig["AdvisoryAI:LlmProviders:Ollama:Enabled"].Should().Be("true"); result.AppliedConfig["AdvisoryAI:LlmProviders:Ollama:Endpoint"].Should().Be("http://localhost:11434"); output.Should().Contain(s => s.Contains("DRY RUN")); } [Fact] public async Task LlmSetupStep_Validate_ReturnsSuccess_WhenDisabled() { // Arrange var step = new LlmSetupStep(); var context = new SetupStepContext { SessionId = "test-session", Runtime = RuntimeEnvironment.Bare, ConfigValues = new Dictionary { ["AdvisoryAI:Enabled"] = "false" } }; // Act var result = await step.ValidateAsync(context); // Assert result.IsValid.Should().BeTrue(); } [Fact] public void AllSetupSteps_HaveUniqueIds_IncludingSprint7_9Steps() { // Arrange var steps = new ISetupStep[] { new DatabaseSetupStep(), new CacheSetupStep(), new VaultSetupStep(), new SettingsStoreSetupStep(), new RegistrySetupStep(), new TelemetrySetupStep(), new AuthoritySetupStep(), new UsersSetupStep(), new NotifySetupStep(), new LlmSetupStep() }; // Assert var ids = steps.Select(s => s.Id).ToList(); ids.Should().OnlyHaveUniqueItems(); } [Fact] public void StepCatalog_ResolvesExecutionOrder_WithAllSteps() { // Arrange var catalog = new SetupStepCatalog(); catalog.Register(new AuthoritySetupStep()); catalog.Register(new UsersSetupStep()); catalog.Register(new DatabaseSetupStep()); catalog.Register(new CacheSetupStep()); catalog.Register(new NotifySetupStep()); catalog.Register(new LlmSetupStep()); // Act var orderedSteps = catalog.ResolveExecutionOrder().ToList(); // Assert orderedSteps.Should().HaveCount(6); // Authority must come before Users (Users depends on Authority) var authorityIndex = orderedSteps.FindIndex(s => s.Id == "authority"); var usersIndex = orderedSteps.FindIndex(s => s.Id == "users"); authorityIndex.Should().BeLessThan(usersIndex); // Database must come before Cache (Cache depends on Database) var databaseIndex = orderedSteps.FindIndex(s => s.Id == "database"); var cacheIndex = orderedSteps.FindIndex(s => s.Id == "cache"); databaseIndex.Should().BeLessThan(cacheIndex); } }