diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index 57a2b11e..4a22e6f2 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs @@ -32,7 +32,6 @@ public static Command CreateCommand( " 2. a365 setup blueprint\n" + " 3. a365 setup permissions mcp\n" + " 4. a365 setup permissions bot\n" + - " 5. a365 setup endpoint\n\n" + "Or run all steps at once:\n" + " a365 setup all # Full setup (includes infrastructure)\n" + " a365 setup all --skip-infrastructure # Skip infrastructure if it already exists"); @@ -42,14 +41,11 @@ public static Command CreateCommand( logger, configService, azureValidator, webAppCreator, platformDetector, executor)); command.AddCommand(BlueprintSubcommand.CreateCommand( - logger, configService, executor, azureValidator, webAppCreator, platformDetector)); + logger, configService, executor, azureValidator, webAppCreator, platformDetector, botConfigurator)); command.AddCommand(PermissionsSubcommand.CreateCommand( logger, configService, executor, graphApiService)); - command.AddCommand(EndpointSubcommand.CreateCommand( - logger, configService, botConfigurator, platformDetector)); - command.AddCommand(AllSubcommand.CreateCommand( logger, configService, executor, botConfigurator, azureValidator, webAppCreator, platformDetector, graphApiService)); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs index debdcd7a..f9023454 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs @@ -156,9 +156,14 @@ public static Command CreateCommand( azureValidator, logger, skipInfrastructure, - true); + true, + configService, + botConfigurator, + platformDetector + ); setupResults.BlueprintCreated = blueprintCreated; + setupResults.MessagingEndpointRegistered = blueprintCreated; if (blueprintCreated) { @@ -178,13 +183,14 @@ public static Command CreateCommand( { throw new SetupValidationException( "Blueprint creation completed but AgentBlueprintId was not saved to configuration. " + - "This is required for the next steps (MCP permissions, Bot permissions, and endpoint registration)."); + "This is required for the next steps (MCP permissions and Bot permissions)."); } } } catch (Exception blueprintEx) { setupResults.BlueprintCreated = false; + setupResults.MessagingEndpointRegistered = false; setupResults.Errors.Add($"Blueprint: {blueprintEx.Message}"); logger.LogError(blueprintEx, "Failed to create blueprint: {Message}", blueprintEx.Message); throw; @@ -245,30 +251,6 @@ public static Command CreateCommand( logger.LogWarning("Setup will continue, but Bot API permissions must be configured manually"); } - // Step 5: Register endpoint and sync - logger.LogInformation(""); - logger.LogInformation("Step 5:"); - logger.LogInformation(""); - - try - { - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - config.FullName, - logger, - configService, - botConfigurator, - platformDetector); - - setupResults.MessagingEndpointRegistered = true; - logger.LogInformation("Blueprint messaging endpoint registered successfully"); - } - catch (Exception endpointEx) - { - setupResults.MessagingEndpointRegistered = false; - setupResults.Errors.Add($"Messaging endpoint: {endpointEx.Message}"); - logger.LogError("Failed to register messaging endpoint: {Message}", endpointEx.Message); - } - // Display verification info and summary logger.LogInformation(""); await SetupHelpers.DisplayVerificationInfoAsync(config, logger); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs index aa63c959..61580e54 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs @@ -5,6 +5,7 @@ using Azure.Identity; using Microsoft.Agents.A365.DevTools.Cli.Constants; using Microsoft.Agents.A365.DevTools.Cli.Exceptions; +using Microsoft.Agents.A365.DevTools.Cli.Helpers; using Microsoft.Agents.A365.DevTools.Cli.Services; using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers; using Microsoft.Extensions.Logging; @@ -34,7 +35,8 @@ public static Command CreateCommand( CommandExecutor executor, IAzureValidator azureValidator, AzureWebAppCreator webAppCreator, - PlatformDetector platformDetector) + PlatformDetector platformDetector, + IBotConfigurator botConfigurator) { var command = new Command("blueprint", "Create agent blueprint (Entra ID application registration)\n" + @@ -78,7 +80,11 @@ await CreateBlueprintImplementationAsync( azureValidator, logger, false, - false); + false, + configService, + botConfigurator, + platformDetector + ); }, configOption, verboseOption, dryRunOption); @@ -93,6 +99,9 @@ public static async Task CreateBlueprintImplementationAsync( ILogger logger, bool skipInfrastructure, bool isSetupAll, + IConfigService configService, + IBotConfigurator botConfigurator, + PlatformDetector platformDetector, CancellationToken cancellationToken = default) { logger.LogInformation(""); @@ -247,17 +256,26 @@ await CreateBlueprintClientSecretAsync( setupConfig, logger); - // Final summary logger.LogInformation(""); logger.LogInformation("Agent blueprint created successfully"); logger.LogInformation("Generated config saved: {Path}", generatedConfigPath); logger.LogInformation(""); + + await RegisterEndpointAndSyncAsync( + configPath: config.FullName, + logger: logger, + configService: configService, + botConfigurator: botConfigurator, + platformDetector: platformDetector); + + // Display verification info and summary + await SetupHelpers.DisplayVerificationInfoAsync(config, logger); + if (!isSetupAll) { logger.LogInformation("Next steps:"); logger.LogInformation(" 1. Run 'a365 setup permissions mcp' to configure MCP permissions"); logger.LogInformation(" 2. Run 'a365 setup permissions bot' to configure Bot API permissions"); - logger.LogInformation(" 3. Run 'a365 setup endpoint' to register messaging endpoint"); } return true; @@ -828,6 +846,73 @@ await File.WriteAllTextAsync( } } + /// + /// Registers blueprint messaging endpoint and syncs project settings. + /// Public method that can be called by AllSubcommand. + /// + public static async Task RegisterEndpointAndSyncAsync( + string configPath, + ILogger logger, + IConfigService configService, + IBotConfigurator botConfigurator, + PlatformDetector platformDetector, + CancellationToken cancellationToken = default) + { + var setupConfig = await configService.LoadAsync(configPath); + + if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId)) + { + logger.LogError("Blueprint ID not found. Please confirm agent blueprint id is in config file."); + Environment.Exit(1); + } + + if (string.IsNullOrWhiteSpace(setupConfig.WebAppName)) + { + logger.LogError("Web App Name not found. Run 'a365 setup infrastructure' first."); + Environment.Exit(1); + } + + logger.LogInformation("Registering blueprint messaging endpoint..."); + logger.LogInformation(""); + + await SetupHelpers.RegisterBlueprintMessagingEndpointAsync( + setupConfig, logger, botConfigurator); + + + setupConfig.Completed = true; + setupConfig.CompletedAt = DateTime.UtcNow; + + await configService.SaveStateAsync(setupConfig); + + logger.LogInformation(""); + logger.LogInformation("Blueprint messaging endpoint registered successfully"); + + // Sync generated config to project settings (appsettings.json or .env) + logger.LogInformation(""); + logger.LogInformation("Syncing configuration to project settings..."); + + var configFileInfo = new FileInfo(configPath); + var generatedConfigPath = Path.Combine( + configFileInfo.DirectoryName ?? Environment.CurrentDirectory, + "a365.generated.config.json"); + + try + { + await ProjectSettingsSyncHelper.ExecuteAsync( + a365ConfigPath: configPath, + a365GeneratedPath: generatedConfigPath, + configService: configService, + platformDetector: platformDetector, + logger: logger); + + logger.LogInformation("Configuration synced to project settings successfully"); + } + catch (Exception syncEx) + { + logger.LogWarning(syncEx, "Project settings sync failed (non-blocking). Please sync settings manually if needed."); + } + } + #region Private Helper Methods private static async Task CreateFederatedIdentityCredentialAsync( diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/EndpointSubcommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/EndpointSubcommand.cs deleted file mode 100644 index d2f6e530..00000000 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/EndpointSubcommand.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Agents.A365.DevTools.Cli.Helpers; -using Microsoft.Agents.A365.DevTools.Cli.Services; -using Microsoft.Extensions.Logging; -using System.CommandLine; -using System.Text.Json; - -namespace Microsoft.Agents.A365.DevTools.Cli.Commands.SetupSubcommands; - -/// -/// Endpoint subcommand - Registers blueprint messaging endpoint (Azure Bot Service) -/// Required Permissions: Azure Subscription Contributor -/// -internal static class EndpointSubcommand -{ - public static Command CreateCommand( - ILogger logger, - IConfigService configService, - IBotConfigurator botConfigurator, - PlatformDetector platformDetector) - { - var command = new Command("endpoint", - "Register blueprint messaging endpoint (Azure Bot Service)\n" + - "Minimum required permissions: Azure Subscription Contributor\n"); - - var configOption = new Option( - ["--config", "-c"], - getDefaultValue: () => new FileInfo("a365.config.json"), - description: "Configuration file path"); - - var verboseOption = new Option( - ["--verbose", "-v"], - description: "Show detailed output"); - - var dryRunOption = new Option( - "--dry-run", - description: "Show what would be done without executing"); - - command.AddOption(configOption); - command.AddOption(verboseOption); - command.AddOption(dryRunOption); - - command.SetHandler(async (config, verbose, dryRun) => - { - var setupConfig = await configService.LoadAsync(config.FullName); - - if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId)) - { - logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first."); - Environment.Exit(1); - } - - if (string.IsNullOrWhiteSpace(setupConfig.WebAppName)) - { - logger.LogError("Web App Name not found. Run 'a365 setup infrastructure' first."); - Environment.Exit(1); - } - - if (dryRun) - { - logger.LogInformation("DRY RUN: Register Messaging Endpoint"); - logger.LogInformation("Would register Bot Service endpoint:"); - logger.LogInformation(" - Endpoint Name: {Name}-endpoint", setupConfig.WebAppName); - logger.LogInformation(" - Messaging URL: https://{Name}.azurewebsites.net/api/messages", setupConfig.WebAppName); - logger.LogInformation(" - Blueprint ID: {Id}", setupConfig.AgentBlueprintId); - logger.LogInformation("Would sync generated configuration to project settings"); - return; - } - - await RegisterEndpointAndSyncAsync( - configPath: config.FullName, - logger: logger, - configService: configService, - botConfigurator: botConfigurator, - platformDetector: platformDetector); - - // Display verification info and summary - await SetupHelpers.DisplayVerificationInfoAsync(config, logger); - - }, configOption, verboseOption, dryRunOption); - - return command; - } - - #region Public Static Implementation Method (for AllSubcommand) - - /// - /// Registers blueprint messaging endpoint and syncs project settings. - /// Public method that can be called by AllSubcommand. - /// - public static async Task RegisterEndpointAndSyncAsync( - string configPath, - ILogger logger, - IConfigService configService, - IBotConfigurator botConfigurator, - PlatformDetector platformDetector, - CancellationToken cancellationToken = default) - { - var setupConfig = await configService.LoadAsync(configPath); - - logger.LogInformation("Registering blueprint messaging endpoint..."); - logger.LogInformation(""); - - await SetupHelpers.RegisterBlueprintMessagingEndpointAsync( - setupConfig, logger, botConfigurator); - - - setupConfig.Completed = true; - setupConfig.CompletedAt = DateTime.UtcNow; - - await configService.SaveStateAsync(setupConfig); - - logger.LogInformation(""); - logger.LogInformation("Blueprint messaging endpoint registered successfully"); - - // Sync generated config to project settings (appsettings.json or .env) - logger.LogInformation(""); - logger.LogInformation("Syncing configuration to project settings..."); - - var configFileInfo = new FileInfo(configPath); - var generatedConfigPath = Path.Combine( - configFileInfo.DirectoryName ?? Environment.CurrentDirectory, - "a365.generated.config.json"); - - try - { - await ProjectSettingsSyncHelper.ExecuteAsync( - a365ConfigPath: configPath, - a365GeneratedPath: generatedConfigPath, - configService: configService, - platformDetector: platformDetector, - logger: logger); - - logger.LogInformation("Configuration synced to project settings successfully"); - } - catch (Exception syncEx) - { - logger.LogWarning(syncEx, "Project settings sync failed (non-blocking). Please sync settings manually if needed."); - } - } - - #endregion -} diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs index 877669af..d74f2a36 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs @@ -152,11 +152,6 @@ public static void DisplaySetupSummary(SetupResults results, ILogger logger) { logger.LogInformation(" - Bot API Permissions: Run 'a365 setup permissions bot' to retry"); } - - if (!results.MessagingEndpointRegistered) - { - logger.LogInformation(" - Messaging Endpoint: Run 'a365 setup endpoint' to retry"); - } } else if (results.HasWarnings) { diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs index d17412b7..c0ee05df 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs @@ -26,6 +26,7 @@ public class BlueprintSubcommandTests private readonly IAzureValidator _mockAzureValidator; private readonly AzureWebAppCreator _mockWebAppCreator; private readonly PlatformDetector _mockPlatformDetector; + private readonly IBotConfigurator _mockBotConfigurator; public BlueprintSubcommandTests() { @@ -37,6 +38,8 @@ public BlueprintSubcommandTests() _mockWebAppCreator = Substitute.ForPartsOf(Substitute.For>()); var mockPlatformDetectorLogger = Substitute.For>(); _mockPlatformDetector = Substitute.ForPartsOf(mockPlatformDetectorLogger); + _mockBotConfigurator = Substitute.For(); + } [Fact] @@ -49,7 +52,8 @@ public void CreateCommand_ShouldHaveCorrectName() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert command.Name.Should().Be("blueprint"); @@ -65,7 +69,8 @@ public void CreateCommand_ShouldHaveDescription() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert command.Description.Should().NotBeNullOrEmpty(); @@ -82,7 +87,8 @@ public void CreateCommand_ShouldHaveConfigOption() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert var configOption = command.Options.FirstOrDefault(o => o.Name == "config"); @@ -101,7 +107,8 @@ public void CreateCommand_ShouldHaveVerboseOption() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert var verboseOption = command.Options.FirstOrDefault(o => o.Name == "verbose"); @@ -120,7 +127,8 @@ public void CreateCommand_ShouldHaveDryRunOption() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert var dryRunOption = command.Options.FirstOrDefault(o => o.Name == "dry-run"); @@ -147,7 +155,8 @@ public async Task DryRun_ShouldLoadConfigAndNotExecute() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -180,7 +189,8 @@ public async Task DryRun_ShouldDisplayBlueprintInformation() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -229,7 +239,10 @@ public async Task CreateBlueprintImplementation_WithMissingDisplayName_ShouldThr _mockAzureValidator, _mockLogger, skipInfrastructure: false, - isSetupAll: false); + isSetupAll: false, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); // Assert - Should return false when consent service fails result.Should().BeFalse(); @@ -259,7 +272,10 @@ public async Task CreateBlueprintImplementation_WithAzureValidationFailure_Shoul _mockAzureValidator, _mockLogger, skipInfrastructure: false, - isSetupAll: false); + isSetupAll: false, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); // Assert result.Should().BeFalse(); @@ -276,7 +292,8 @@ public void CommandDescription_ShouldMentionRequiredPermissions() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert command.Description.Should().Contain("Agent ID Developer"); @@ -302,7 +319,8 @@ public async Task DryRun_WithCustomConfigPath_ShouldLoadCorrectFile() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -336,7 +354,8 @@ public async Task DryRun_ShouldNotCreateServicePrincipal() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -362,7 +381,8 @@ public void CreateCommand_ShouldHandleAllOptions() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert - Verify all expected options are present command.Options.Should().HaveCountGreaterOrEqualTo(3); @@ -386,7 +406,8 @@ public async Task DryRun_WithMissingConfig_ShouldHandleGracefully() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -406,7 +427,8 @@ public void CreateCommand_DefaultConfigPath_ShouldBeA365ConfigJson() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert - Verify the config option exists and has expected aliases var configOption = command.Options.First(o => o.Name == "config"); @@ -439,7 +461,10 @@ public async Task CreateBlueprintImplementation_ShouldLogProgressMessages() _mockAzureValidator, _mockLogger, skipInfrastructure: false, - isSetupAll: false); + isSetupAll: false, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); // Assert result.Should().BeFalse(); @@ -463,7 +488,8 @@ public void CommandDescription_ShouldBeInformativeAndActionable() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert - Verify description provides context and guidance command.Description.Should().NotBeNullOrEmpty(); @@ -489,7 +515,8 @@ public async Task DryRun_WithVerboseFlag_ShouldSucceed() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -521,7 +548,8 @@ public async Task DryRun_ShouldShowWhatWouldBeDone() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); var parser = new CommandLineBuilder(command).Build(); var testConsole = new TestConsole(); @@ -551,10 +579,435 @@ public void CreateCommand_ShouldBeUsableInCommandPipeline() _mockExecutor, _mockAzureValidator, _mockWebAppCreator, - _mockPlatformDetector); + _mockPlatformDetector, + _mockBotConfigurator); // Assert - Verify command can be added to a parser var parser = new CommandLineBuilder(command).Build(); parser.Should().NotBeNull(); } + + #region Endpoint validation Tests (Testing logic without parser) + + [Fact] + public async Task ValidationLogic_WithMissingBlueprintId_ShouldLogError() + { + // Arrange + var config = new Agent365Config + { + TenantId = "test-tenant", + AgentBlueprintId = "", // Missing blueprint ID + WebAppName = "test-webapp" + }; + + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + // Act - Load config and validate + var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); + + // Assert - Verify validation would catch this + loadedConfig.AgentBlueprintId.Should().BeEmpty(); + // In the actual command handler, Environment.Exit(1) would be called + } + + [Fact] + public async Task ValidationLogic_WithMissingWebAppName_ShouldLogError() + { + // Arrange + var config = new Agent365Config + { + TenantId = "test-tenant", + AgentBlueprintId = "blueprint-123", + WebAppName = "" // Missing web app name + }; + + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + // Act + var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); + + // Assert + loadedConfig.WebAppName.Should().BeEmpty(); + // In the actual command handler, Environment.Exit(1) would be called + } + + [Fact] + public async Task DryRunLogic_ShouldNotExecuteRegistration() + { + // Arrange + var config = new Agent365Config + { + TenantId = "test-tenant", + AgentBlueprintId = "blueprint-123", + WebAppName = "test-webapp" + }; + + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + // Act - Simulate dry-run logic (loading config but not executing) + var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); + + // Assert - Verify config was loaded + loadedConfig.Should().NotBeNull(); + loadedConfig.AgentBlueprintId.Should().Be("blueprint-123"); + loadedConfig.WebAppName.Should().Be("test-webapp"); + + // Verify no bot configuration was attempted + await _mockBotConfigurator.DidNotReceiveWithAnyArgs() + .CreateEndpointWithAgentBlueprintAsync(default!, default!, default!, default!, default!); + } + + [Fact] + public void DryRunDisplay_ShouldShowEndpointInfo() + { + // Arrange + var config = new Agent365Config + { + TenantId = "test-tenant", + AgentBlueprintId = "blueprint-456", + WebAppName = "my-agent-webapp" + }; + + // Act - Simulate what dry-run would display + var endpointName = $"{config.WebAppName}-endpoint"; + var messagingUrl = $"https://{config.WebAppName}.azurewebsites.net/api/messages"; + + // Assert + endpointName.Should().Be("my-agent-webapp-endpoint"); + messagingUrl.Should().Be("https://my-agent-webapp.azurewebsites.net/api/messages"); + } + + [Fact] + public void DryRunDisplay_ShouldShowMessagingUrl() + { + // Arrange + var config = new Agent365Config + { + TenantId = "test-tenant", + AgentBlueprintId = "blueprint-789", + WebAppName = "production-agent" + }; + + // Act - Simulate messaging URL generation + var messagingUrl = $"https://{config.WebAppName}.azurewebsites.net/api/messages"; + + // Assert + messagingUrl.Should().Contain("production-agent.azurewebsites.net/api/messages"); + } + + #endregion + + #region RegisterEndpointAndSyncAsync Tests + + [Fact] + public async Task RegisterEndpointAndSyncAsync_WithValidConfig_ShouldSucceed() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + AgentBlueprintId = "blueprint-123", + WebAppName = "test-webapp", + Location = "eastus", + DeploymentProjectPath = Path.GetTempPath() + }; + + var testId = Guid.NewGuid().ToString(); + var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); + var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); + + // Create temporary generated config file + await File.WriteAllTextAsync(generatedPath, "{}"); + + try + { + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + await BlueprintSubcommand.RegisterEndpointAndSyncAsync( + configPath, + _mockLogger, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); + + // Assert + await _mockBotConfigurator.Received(1).CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), + config.Location, + Arg.Is(s => s.Contains("test-webapp.azurewebsites.net")), + Arg.Any(), + config.AgentBlueprintId); + + await _mockConfigService.Received(1).SaveStateAsync(Arg.Any(), Arg.Any()); + } + finally + { + // Cleanup + if (File.Exists(generatedPath)) + { + File.Delete(generatedPath); + } + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + } + } + + [Fact] + public async Task RegisterEndpointAndSyncAsync_ShouldSetCompletedFlag() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + AgentBlueprintId = "blueprint-456", + WebAppName = "test-webapp", + Location = "westus", + DeploymentProjectPath = Path.GetTempPath() + }; + + var testId = Guid.NewGuid().ToString(); + var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); + var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); + await File.WriteAllTextAsync(generatedPath, "{}"); + + try + { + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + Agent365Config? savedConfig = null; + _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask) + .AndDoes(callInfo => savedConfig = callInfo.Arg()); + + _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + await BlueprintSubcommand.RegisterEndpointAndSyncAsync( + configPath, + _mockLogger, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); + + // Assert + savedConfig.Should().NotBeNull(); + savedConfig!.Completed.Should().BeTrue(); + savedConfig.CompletedAt.Should().NotBeNull(); + savedConfig.CompletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(1)); + } + finally + { + if (File.Exists(generatedPath)) + { + File.Delete(generatedPath); + } + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + } + } + + [Fact] + public async Task RegisterEndpointAndSyncAsync_ShouldLogProgressMessages() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + AgentBlueprintId = "blueprint-789", + WebAppName = "test-webapp", + Location = "eastus", + DeploymentProjectPath = Path.GetTempPath() + }; + + var testId = Guid.NewGuid().ToString(); + var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); + var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); + await File.WriteAllTextAsync(generatedPath, "{}"); + + try + { + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + await BlueprintSubcommand.RegisterEndpointAndSyncAsync( + configPath, + _mockLogger, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); + + // Assert + _mockLogger.Received().Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Registering blueprint messaging endpoint")), + Arg.Any(), + Arg.Any>()); + + _mockLogger.Received().Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("registered successfully")), + Arg.Any(), + Arg.Any>()); + } + finally + { + if (File.Exists(generatedPath)) + { + File.Delete(generatedPath); + } + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + } + } + + [Fact] + public async Task RegisterEndpointAndSyncAsync_WhenSyncFails_ShouldLogWarningButContinue() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + AgentBlueprintId = "blueprint-123", + WebAppName = "test-webapp", + Location = "eastus", + DeploymentProjectPath = "non-existent-path" // This will cause sync to skip with a warning + }; + + var testId = Guid.NewGuid().ToString(); + var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); + var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); + await File.WriteAllTextAsync(generatedPath, "{}"); + + try + { + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act - should not throw + await BlueprintSubcommand.RegisterEndpointAndSyncAsync( + configPath, + _mockLogger, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); + + // Assert - ProjectSettingsSyncHelper logs a warning when deploymentProjectPath doesn't exist + _mockLogger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Project settings sync failed") && o.ToString()!.Contains("non-blocking")), + Arg.Any(), + Arg.Any>()); + } + finally + { + if (File.Exists(generatedPath)) + { + File.Delete(generatedPath); + } + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + } + } + + [Fact] + public async Task RegisterEndpointAndSyncAsync_ShouldUpdateBotConfigurationInAgent365Config() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + AgentBlueprintId = "blueprint-123", + WebAppName = "test-webapp", + Location = "eastus", + DeploymentProjectPath = Path.GetTempPath() + }; + + var testId = Guid.NewGuid().ToString(); + var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); + var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); + await File.WriteAllTextAsync(generatedPath, "{}"); + + try + { + _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(config)); + + Agent365Config? savedConfig = null; + _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask) + .AndDoes(callInfo => savedConfig = callInfo.Arg()); + + _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + await BlueprintSubcommand.RegisterEndpointAndSyncAsync( + configPath, + _mockLogger, + _mockConfigService, + _mockBotConfigurator, + _mockPlatformDetector); + + // Assert - Verify bot configuration was updated in config + savedConfig.Should().NotBeNull(); + savedConfig!.BotId.Should().Be(config.AgentBlueprintId); + savedConfig.BotMsaAppId.Should().Be(config.AgentBlueprintId); + savedConfig.BotMessagingEndpoint.Should().Contain("test-webapp.azurewebsites.net"); + } + finally + { + if (File.Exists(generatedPath)) + { + File.Delete(generatedPath); + } + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + } + } + + #endregion } diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/EndpointSubcommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/EndpointSubcommandTests.cs deleted file mode 100644 index 120b820e..00000000 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/EndpointSubcommandTests.cs +++ /dev/null @@ -1,591 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using FluentAssertions; -using Microsoft.Agents.A365.DevTools.Cli.Commands.SetupSubcommands; -using Microsoft.Agents.A365.DevTools.Cli.Models; -using Microsoft.Agents.A365.DevTools.Cli.Services; -using Microsoft.Extensions.Logging; -using NSubstitute; -using System.CommandLine; -using Xunit; - -namespace Microsoft.Agents.A365.DevTools.Cli.Tests.Commands; - -/// -/// Unit tests for Endpoint subcommand -/// -[Collection("Sequential")] -public class EndpointSubcommandTests -{ - private readonly ILogger _mockLogger; - private readonly IConfigService _mockConfigService; - private readonly IBotConfigurator _mockBotConfigurator; - private readonly PlatformDetector _mockPlatformDetector; - - public EndpointSubcommandTests() - { - _mockLogger = Substitute.For(); - _mockConfigService = Substitute.For(); - _mockBotConfigurator = Substitute.For(); - - // Create a simple mock without using ForPartsOf which might be causing issues - _mockPlatformDetector = Substitute.For(Substitute.For>()); - } - - #region Command Structure Tests - - [Fact] - public void CreateCommand_ShouldHaveConfigOption() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - var configOption = command.Options.FirstOrDefault(o => o.Name == "config"); - configOption.Should().NotBeNull(); - configOption!.Aliases.Should().Contain("--config"); - configOption.Aliases.Should().Contain("-c"); - } - - [Fact] - public void CreateCommand_ShouldHaveVerboseOption() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - var verboseOption = command.Options.FirstOrDefault(o => o.Name == "verbose"); - verboseOption.Should().NotBeNull(); - verboseOption!.Aliases.Should().Contain("--verbose"); - verboseOption.Aliases.Should().Contain("-v"); - } - - [Fact] - public void CreateCommand_ShouldHaveDryRunOption() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - var dryRunOption = command.Options.FirstOrDefault(o => o.Name == "dry-run"); - dryRunOption.Should().NotBeNull(); - dryRunOption!.Aliases.Should().Contain("--dry-run"); - } - - [Fact] - public void CommandDescription_ShouldMentionRequiredPermissions() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - command.Description.Should().Contain("Azure Subscription Contributor"); - } - - [Fact] - public void CreateCommand_ShouldBeUsableInCommandPipeline() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - Verify command can be created and has expected properties - command.Should().NotBeNull(); - command.Name.Should().Be("endpoint"); - command.Options.Should().HaveCountGreaterOrEqualTo(3); - } - - [Fact] - public void CreateCommand_ShouldHandleAllOptions() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - Verify all expected options are present - command.Options.Should().HaveCountGreaterOrEqualTo(3); - - var optionNames = command.Options.Select(o => o.Name).ToList(); - optionNames.Should().Contain("config"); - optionNames.Should().Contain("verbose"); - optionNames.Should().Contain("dry-run"); - } - - [Fact] - public void CommandDescription_ShouldBeInformativeAndActionable() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - command.Description.Should().NotBeNullOrEmpty(); - command.Description.Should().ContainAny("endpoint", "messaging", "Bot Service"); - } - - [Fact] - public void CommandDescription_ShouldMentionAzureBotService() - { - // Act - var command = EndpointSubcommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - command.Description.Should().Contain("Azure Bot Service"); - } - - #endregion - - #region Validation Tests (Testing logic without parser) - - [Fact] - public async Task ValidationLogic_WithMissingBlueprintId_ShouldLogError() - { - // Arrange - var config = new Agent365Config - { - TenantId = "test-tenant", - AgentBlueprintId = "", // Missing blueprint ID - WebAppName = "test-webapp" - }; - - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - // Act - Load config and validate - var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); - - // Assert - Verify validation would catch this - loadedConfig.AgentBlueprintId.Should().BeEmpty(); - // In the actual command handler, Environment.Exit(1) would be called - } - - [Fact] - public async Task ValidationLogic_WithMissingWebAppName_ShouldLogError() - { - // Arrange - var config = new Agent365Config - { - TenantId = "test-tenant", - AgentBlueprintId = "blueprint-123", - WebAppName = "" // Missing web app name - }; - - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - // Act - var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); - - // Assert - loadedConfig.WebAppName.Should().BeEmpty(); - // In the actual command handler, Environment.Exit(1) would be called - } - - [Fact] - public async Task DryRunLogic_ShouldNotExecuteRegistration() - { - // Arrange - var config = new Agent365Config - { - TenantId = "test-tenant", - AgentBlueprintId = "blueprint-123", - WebAppName = "test-webapp" - }; - - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - // Act - Simulate dry-run logic (loading config but not executing) - var loadedConfig = await _mockConfigService.LoadAsync("test-config.json"); - - // Assert - Verify config was loaded - loadedConfig.Should().NotBeNull(); - loadedConfig.AgentBlueprintId.Should().Be("blueprint-123"); - loadedConfig.WebAppName.Should().Be("test-webapp"); - - // Verify no bot configuration was attempted - await _mockBotConfigurator.DidNotReceiveWithAnyArgs() - .CreateEndpointWithAgentBlueprintAsync(default!, default!, default!, default!, default!); - } - - [Fact] - public void DryRunDisplay_ShouldShowEndpointInfo() - { - // Arrange - var config = new Agent365Config - { - TenantId = "test-tenant", - AgentBlueprintId = "blueprint-456", - WebAppName = "my-agent-webapp" - }; - - // Act - Simulate what dry-run would display - var endpointName = $"{config.WebAppName}-endpoint"; - var messagingUrl = $"https://{config.WebAppName}.azurewebsites.net/api/messages"; - - // Assert - endpointName.Should().Be("my-agent-webapp-endpoint"); - messagingUrl.Should().Be("https://my-agent-webapp.azurewebsites.net/api/messages"); - } - - [Fact] - public void DryRunDisplay_ShouldShowMessagingUrl() - { - // Arrange - var config = new Agent365Config - { - TenantId = "test-tenant", - AgentBlueprintId = "blueprint-789", - WebAppName = "production-agent" - }; - - // Act - Simulate messaging URL generation - var messagingUrl = $"https://{config.WebAppName}.azurewebsites.net/api/messages"; - - // Assert - messagingUrl.Should().Contain("production-agent.azurewebsites.net/api/messages"); - } - - #endregion - - #region RegisterEndpointAndSyncAsync Tests - - [Fact] - public async Task RegisterEndpointAndSyncAsync_WithValidConfig_ShouldSucceed() - { - // Arrange - var config = new Agent365Config - { - TenantId = "00000000-0000-0000-0000-000000000000", - AgentBlueprintId = "blueprint-123", - WebAppName = "test-webapp", - Location = "eastus", - DeploymentProjectPath = Path.GetTempPath() - }; - - var testId = Guid.NewGuid().ToString(); - var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); - var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); - - // Create temporary generated config file - await File.WriteAllTextAsync(generatedPath, "{}"); - - try - { - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask); - - _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(true); - - // Act - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - configPath, - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - await _mockBotConfigurator.Received(1).CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), - config.Location, - Arg.Is(s => s.Contains("test-webapp.azurewebsites.net")), - Arg.Any(), - config.AgentBlueprintId); - - await _mockConfigService.Received(1).SaveStateAsync(Arg.Any(), Arg.Any()); - } - finally - { - // Cleanup - if (File.Exists(generatedPath)) - { - File.Delete(generatedPath); - } - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } - - [Fact] - public async Task RegisterEndpointAndSyncAsync_ShouldSetCompletedFlag() - { - // Arrange - var config = new Agent365Config - { - TenantId = "00000000-0000-0000-0000-000000000000", - AgentBlueprintId = "blueprint-456", - WebAppName = "test-webapp", - Location = "westus", - DeploymentProjectPath = Path.GetTempPath() - }; - - var testId = Guid.NewGuid().ToString(); - var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); - var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); - await File.WriteAllTextAsync(generatedPath, "{}"); - - try - { - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - Agent365Config? savedConfig = null; - _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask) - .AndDoes(callInfo => savedConfig = callInfo.Arg()); - - _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(true); - - // Act - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - configPath, - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - savedConfig.Should().NotBeNull(); - savedConfig!.Completed.Should().BeTrue(); - savedConfig.CompletedAt.Should().NotBeNull(); - savedConfig.CompletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(1)); - } - finally - { - if (File.Exists(generatedPath)) - { - File.Delete(generatedPath); - } - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } - - [Fact] - public async Task RegisterEndpointAndSyncAsync_ShouldLogProgressMessages() - { - // Arrange - var config = new Agent365Config - { - TenantId = "00000000-0000-0000-0000-000000000000", - AgentBlueprintId = "blueprint-789", - WebAppName = "test-webapp", - Location = "eastus", - DeploymentProjectPath = Path.GetTempPath() - }; - - var testId = Guid.NewGuid().ToString(); - var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); - var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); - await File.WriteAllTextAsync(generatedPath, "{}"); - - try - { - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask); - - _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(true); - - // Act - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - configPath, - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - _mockLogger.Received().Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(o => o.ToString()!.Contains("Registering blueprint messaging endpoint")), - Arg.Any(), - Arg.Any>()); - - _mockLogger.Received().Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(o => o.ToString()!.Contains("registered successfully")), - Arg.Any(), - Arg.Any>()); - } - finally - { - if (File.Exists(generatedPath)) - { - File.Delete(generatedPath); - } - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } - - [Fact] - public async Task RegisterEndpointAndSyncAsync_WhenSyncFails_ShouldLogWarningButContinue() - { - // Arrange - var config = new Agent365Config - { - TenantId = "00000000-0000-0000-0000-000000000000", - AgentBlueprintId = "blueprint-123", - WebAppName = "test-webapp", - Location = "eastus", - DeploymentProjectPath = "non-existent-path" // This will cause sync to skip with a warning - }; - - var testId = Guid.NewGuid().ToString(); - var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); - var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); - await File.WriteAllTextAsync(generatedPath, "{}"); - - try - { - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask); - - _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(true); - - // Act - should not throw - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - configPath, - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - ProjectSettingsSyncHelper logs a warning when deploymentProjectPath doesn't exist - _mockLogger.Received().Log( - LogLevel.Warning, - Arg.Any(), - Arg.Is(o => o.ToString()!.Contains("Project settings sync failed") && o.ToString()!.Contains("non-blocking")), - Arg.Any(), - Arg.Any>()); - } - finally - { - if (File.Exists(generatedPath)) - { - File.Delete(generatedPath); - } - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } - - [Fact] - public async Task RegisterEndpointAndSyncAsync_ShouldUpdateBotConfigurationInAgent365Config() - { - // Arrange - var config = new Agent365Config - { - TenantId = "00000000-0000-0000-0000-000000000000", - AgentBlueprintId = "blueprint-123", - WebAppName = "test-webapp", - Location = "eastus", - DeploymentProjectPath = Path.GetTempPath() - }; - - var testId = Guid.NewGuid().ToString(); - var configPath = Path.Combine(Path.GetTempPath(), $"test-config-{testId}.json"); - var generatedPath = Path.Combine(Path.GetTempPath(), $"a365.generated.config-{testId}.json"); - await File.WriteAllTextAsync(generatedPath, "{}"); - - try - { - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(config)); - - Agent365Config? savedConfig = null; - _mockConfigService.SaveStateAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask) - .AndDoes(callInfo => savedConfig = callInfo.Arg()); - - _mockBotConfigurator.CreateEndpointWithAgentBlueprintAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(true); - - // Act - await EndpointSubcommand.RegisterEndpointAndSyncAsync( - configPath, - _mockLogger, - _mockConfigService, - _mockBotConfigurator, - _mockPlatformDetector); - - // Assert - Verify bot configuration was updated in config - savedConfig.Should().NotBeNull(); - savedConfig!.BotId.Should().Be(config.AgentBlueprintId); - savedConfig.BotMsaAppId.Should().Be(config.AgentBlueprintId); - savedConfig.BotMessagingEndpoint.Should().Contain("test-webapp.azurewebsites.net"); - } - finally - { - if (File.Exists(generatedPath)) - { - File.Delete(generatedPath); - } - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } - - #endregion -} diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs index e896aa08..7b27fffb 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs @@ -164,7 +164,6 @@ public void SetupCommand_HasRequiredSubcommands() subcommandNames.Should().Contain("infrastructure", "Setup should have infrastructure subcommand"); subcommandNames.Should().Contain("blueprint", "Setup should have blueprint subcommand"); subcommandNames.Should().Contain("permissions", "Setup should have permissions subcommand"); - subcommandNames.Should().Contain("endpoint", "Setup should have endpoint subcommand"); subcommandNames.Should().Contain("all", "Setup should have all subcommand"); } @@ -312,47 +311,5 @@ public async Task BlueprintSubcommand_DryRun_CompletesSuccessfully() // Verify config was loaded in dry-run mode await _mockConfigService.Received(1).LoadAsync(Arg.Any(), Arg.Any()); } - - [Fact] - public async Task EndpointSubcommand_DryRun_CompletesSuccessfully() - { - // Arrange - var config = new Agent365Config - { - TenantId = "tenant", - SubscriptionId = "sub", - ResourceGroup = "rg", - Location = "eastus", - AppServicePlanName = "plan", - WebAppName = "web", - AgentIdentityDisplayName = "agent", - DeploymentProjectPath = ".", - AgentBlueprintId = "blueprint-id" - }; - - _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(config)); - - var command = SetupCommand.CreateCommand( - _mockLogger, - _mockConfigService, - _mockExecutor, - _mockDeploymentService, - _mockBotConfigurator, - _mockAzureValidator, - _mockWebAppCreator, - _mockPlatformDetector, - _mockGraphApiService); - var parser = new CommandLineBuilder(command).Build(); - var testConsole = new TestConsole(); - - // Act - var result = await parser.InvokeAsync("endpoint --dry-run", testConsole); - - // Assert - Assert.Equal(0, result); - - // Verify config was loaded in dry-run mode - await _mockConfigService.Received(1).LoadAsync(Arg.Any(), Arg.Any()); - } }