using System; using System.IO; using System.Reflection; using GmRelay.DiscordBot.Features.Sessions; using NetCord.Services.ApplicationCommands; namespace GmRelay.Bot.Tests.Discord; public sealed class DiscordStartupTests { private static string GetRepoRoot() { var dir = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Directory.Build.props"))) { dir = Directory.GetParent(dir)?.FullName; } return dir ?? throw new InvalidOperationException("Could not find repo root"); } [Fact] public void Program_ShouldValidateDiscordTokenBeforeRunning() { var program = ReadProgram(); Assert.Contains("GetRequiredSection(\"Discord\")", program); Assert.Contains("DiscordOptions", program); Assert.Contains(".Validate()", program); } [Fact] public void Program_ShouldRegisterServiceDefaultsAndPostgresDataSource() { var program = ReadProgram(); Assert.Contains("builder.AddServiceDefaults()", program); Assert.Contains("ConnectionStrings:gmrelaydb is required", program); Assert.Contains("NpgsqlDataSource", program); Assert.Contains("SecretRedactor.RedactConnectionString", program); } [Fact] public void Program_ShouldRegisterNetCordGatewayApplicationCommandsAndComponents() { var program = ReadProgram(); Assert.Contains(".AddDiscordGateway", program); Assert.Contains(".AddApplicationCommands", program); Assert.Contains(".AddComponentInteractions", program); Assert.Contains(".AddGatewayHandlers", program); Assert.Contains("AddSlashCommand", program); Assert.Contains("AddModules(typeof(Program).Assembly)", program); } [Theory] [InlineData(typeof(DiscordNewSessionCommand), "newsession")] [InlineData(typeof(DiscordListSessionsCommand), "listsessions")] [InlineData(typeof(DiscordRescheduleCommand), "reschedule")] public void DiscordSessionSlashCommands_ShouldBeDeclaredOnModuleMethods(Type moduleType, string commandName) { var executeMethod = moduleType.GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.Public); Assert.NotNull(executeMethod); var methodAttribute = Assert.Single(executeMethod.GetCustomAttributes(inherit: false)); var nameProperty = typeof(SlashCommandAttribute).GetProperty("Name") ?? throw new InvalidOperationException("SlashCommandAttribute should expose command name."); Assert.Equal(commandName, nameProperty.GetValue(methodAttribute)); Assert.Empty(moduleType.GetCustomAttributes(inherit: false)); } [Fact] public void DiscordSessionSlashCommands_ShouldBeDiscoverableByNetCordService() { var service = new ApplicationCommandService(); service.AddModules(typeof(DiscordNewSessionCommand).Assembly); var commandNames = service.GetCommands() .Select(command => command.Name) .ToArray(); Assert.Contains("newsession", commandNames); Assert.Contains("listsessions", commandNames); Assert.Contains("reschedule", commandNames); } [Fact] public void LifecycleLogger_ShouldLogGatewayLifecycleEventsWithoutTokenValues() { var repoRoot = GetRepoRoot(); var loggerPath = Path.Combine( repoRoot, "src", "GmRelay.DiscordBot", "Infrastructure", "Logging", "DiscordGatewayLifecycleLogger.cs"); Assert.True(File.Exists(loggerPath), "Discord gateway lifecycle logger should exist."); var logger = File.ReadAllText(loggerPath); Assert.Contains("IReadyGatewayHandler", logger); Assert.Contains("IDisconnectGatewayHandler", logger); Assert.Contains("IResumeGatewayHandler", logger); Assert.Contains("LogInformation", logger); Assert.DoesNotContain("Token", logger); } [Fact] public void Program_ShouldRegisterDiscordSessionHandlers() { var program = ReadProgram(); Assert.Contains("DiscordListSessionsHandler", program); Assert.Contains("DiscordNewSessionHandler", program); Assert.Contains("JoinSessionHandler", program); Assert.Contains("LeaveSessionHandler", program); Assert.Contains("DiscordPermissionChecker", program); Assert.Contains("DiscordPlatformMessenger", program); Assert.Contains("IPlatformMessenger", program); Assert.Contains("PlatformSchedulerOptions(PlatformKind.Discord)", program); Assert.Contains("AddHostedService", program); Assert.Contains("HandleRsvpHandler", program); } [Fact] public void Program_ShouldRegisterDiscordHealthCheckHostedService() { var program = ReadProgram(); Assert.Contains("DiscordHealthCheckHostedService", program); Assert.Contains("AddHostedService", program); } private static string ReadProgram() { var repoRoot = GetRepoRoot(); return File.ReadAllText(Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Program.cs")); } }