feat(platform): route scheduler notifications through platform messenger
PR Checks / test-and-build (pull_request) Successful in 7m9s

This commit is contained in:
2026-05-21 12:30:35 +03:00
parent 5dbec1a0a4
commit 2a707e4825
49 changed files with 2158 additions and 846 deletions
@@ -20,11 +20,14 @@ public sealed class DiscordNewSessionHandlerTests
[Fact]
public void ParseTimeInput_ShouldParseDiscordDateFormat()
{
var result = DiscordNewSessionHandler.ParseTimeInput("2026-05-20 19:30");
var expected = FutureDateAt1930();
var result = DiscordNewSessionHandler.ParseTimeInput(
expected.ToString("yyyy-MM-dd HH:mm", System.Globalization.CultureInfo.InvariantCulture));
Assert.True(result.IsSuccess);
Assert.Equal(2026, result.Value.Year);
Assert.Equal(5, result.Value.Month);
Assert.Equal(20, result.Value.Day);
Assert.Equal(expected.Year, result.Value.Year);
Assert.Equal(expected.Month, result.Value.Month);
Assert.Equal(expected.Day, result.Value.Day);
Assert.Equal(19, result.Value.Hour);
Assert.Equal(30, result.Value.Minute);
}
@@ -39,11 +42,14 @@ public sealed class DiscordNewSessionHandlerTests
[Fact]
public void ParseTimeInput_ShouldParseRussianDateFormat()
{
var result = DiscordNewSessionHandler.ParseTimeInput("20.05.2026 19:30");
var expected = FutureDateAt1930();
var result = DiscordNewSessionHandler.ParseTimeInput(
expected.ToString("dd.MM.yyyy HH:mm", System.Globalization.CultureInfo.InvariantCulture));
Assert.True(result.IsSuccess);
Assert.Equal(2026, result.Value.Year);
Assert.Equal(5, result.Value.Month);
Assert.Equal(20, result.Value.Day);
Assert.Equal(expected.Year, result.Value.Year);
Assert.Equal(expected.Month, result.Value.Month);
Assert.Equal(expected.Day, result.Value.Day);
}
[Fact]
@@ -141,4 +147,17 @@ public sealed class DiscordNewSessionHandlerTests
Assert.Contains("DiscordSessionBatchRenderer.Render", source, StringComparison.Ordinal);
Assert.Contains("WithEmbeds", source, StringComparison.Ordinal);
}
private static DateTimeOffset FutureDateAt1930()
{
var future = DateTimeOffset.UtcNow.AddDays(7);
return new DateTimeOffset(
future.Year,
future.Month,
future.Day,
19,
30,
0,
TimeSpan.Zero);
}
}
@@ -32,6 +32,21 @@ public sealed class DiscordPlatformMessengerTests
Assert.Contains("interactionReplies.Store(reply)", source, StringComparison.Ordinal);
}
[Fact]
public async Task DiscordPlatformMessenger_ShouldSupportSchedulerNotifications()
{
var source = await ReadRepositoryFileAsync("src/GmRelay.DiscordBot/Infrastructure/Discord/DiscordPlatformMessenger.cs");
Assert.Contains("SendConfirmationRequestAsync", source, StringComparison.Ordinal);
Assert.Contains("UpdateConfirmationRequestAsync", source, StringComparison.Ordinal);
Assert.Contains("SendJoinLinkNotificationAsync", source, StringComparison.Ordinal);
Assert.Contains("SendDirectSessionNotificationAsync", source, StringComparison.Ordinal);
Assert.Contains("UpdateRescheduleVoteAsync", source, StringComparison.Ordinal);
Assert.Contains("DiscordSessionBatchRenderer", source, StringComparison.Ordinal);
Assert.Contains("DiscordRescheduleVotingRenderer", source, StringComparison.Ordinal);
Assert.Contains("GetDMChannelAsync", source, StringComparison.Ordinal);
}
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
@@ -61,7 +61,7 @@ public sealed class DiscordProjectStructureTests
var prChecks = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "pr-checks.yml"));
var deploy = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml"));
Assert.Contains("gmrelay-discord-bot:2.6.0", compose);
Assert.Contains("gmrelay-discord-bot:2.7.0", compose);
Assert.Contains("Discord__Token=${DISCORD_BOT_TOKEN:?Set DISCORD_BOT_TOKEN in .env}", compose);
Assert.Contains("src/GmRelay.DiscordBot/Dockerfile", deploy);
Assert.Contains("DISCORD_BOT_TOKEN", deploy);
@@ -75,13 +75,13 @@ public sealed class DiscordProjectStructureTests
{
var repoRoot = GetRepoRoot();
Assert.Contains("<Version>2.6.0</Version>", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props")));
Assert.Contains("VERSION: 2.6.0", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml")));
Assert.Contains("gmrelay-bot:2.6.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains("gmrelay-web:2.6.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains("gmrelay-discord-bot:2.6.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains("<Version>2.7.0</Version>", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props")));
Assert.Contains("VERSION: 2.7.0", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml")));
Assert.Contains("gmrelay-bot:2.7.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains("gmrelay-web:2.7.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains("gmrelay-discord-bot:2.7.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml")));
Assert.Contains(
"v2.6.0",
"v2.7.0",
File.ReadAllText(Path.Combine(repoRoot, "src", "GmRelay.Web", "Components", "Layout", "NavMenu.razor")));
}
}
@@ -0,0 +1,31 @@
namespace GmRelay.Bot.Tests.Discord;
public sealed class DiscordRescheduleDeadlineBoundaryTests
{
[Fact]
public async Task DiscordDeadlineService_ShouldUsePlatformMessengerForMessageUpdates()
{
var source = await ReadRepositoryFileAsync(
"src/GmRelay.DiscordBot/Features/Sessions/DiscordRescheduleVotingDeadlineService.cs");
Assert.DoesNotContain("RestClient", source, StringComparison.Ordinal);
Assert.DoesNotContain("ModifyMessageAsync", source, StringComparison.Ordinal);
Assert.Contains("UpdateRescheduleVoteAsync", source, StringComparison.Ordinal);
Assert.Contains("IPlatformMessenger", source, StringComparison.Ordinal);
}
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
return await File.ReadAllTextAsync(candidate);
directory = directory.Parent;
}
throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
}
}
@@ -21,6 +21,16 @@ public sealed class DiscordSessionInteractionModuleSourceTests
Assert.Contains("MessageFlags.Ephemeral", source, StringComparison.Ordinal);
}
[Fact]
public async Task Module_ShouldRouteRsvpButtonsToNeutralHandler()
{
var source = await ReadRepositoryFileAsync("src/GmRelay.DiscordBot/Features/Sessions/DiscordSessionInteractionModule.cs");
Assert.Contains("[ComponentInteraction(\"rsvp\")", source, StringComparison.Ordinal);
Assert.Contains("HandleRsvpHandler", source, StringComparison.Ordinal);
Assert.Contains("PlatformKind.Discord", source, StringComparison.Ordinal);
}
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
@@ -82,6 +82,9 @@ public sealed class DiscordStartupTests
Assert.Contains("DiscordPermissionChecker", program);
Assert.Contains("DiscordPlatformMessenger", program);
Assert.Contains("IPlatformMessenger", program);
Assert.Contains("PlatformSchedulerOptions(PlatformKind.Discord)", program);
Assert.Contains("AddHostedService<SessionSchedulerService>", program);
Assert.Contains("HandleRsvpHandler", program);
}
private static string ReadProgram()