d55003a2a9
PR Checks / test-and-build (pull_request) Successful in 5m59s
- DiscordNewSessionCommand: on success, renders session details via DiscordSessionBatchRenderer.Render() with embeds and action rows. - DiscordNewSessionCommand: uses Discord emoji shortcodes for error and success messages (✅, ⛔, 💥). - DiscordNewSessionHandlerTests: added 7 source-level structural tests verifying Dapper usage, NpgsqlDataSource, permission checks, platform neutrality, transaction safety, CancellationToken usage, and embed rendering in the command. Refs issue #28 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
145 lines
5.4 KiB
C#
145 lines
5.4 KiB
C#
using GmRelay.DiscordBot.Features.Sessions;
|
|
|
|
namespace GmRelay.Bot.Tests.Discord;
|
|
|
|
public sealed class DiscordNewSessionHandlerTests
|
|
{
|
|
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");
|
|
}
|
|
|
|
// --- Runtime tests for ParseTimeInput (static, no DB) ---
|
|
|
|
[Fact]
|
|
public void ParseTimeInput_ShouldParseDiscordDateFormat()
|
|
{
|
|
var result = DiscordNewSessionHandler.ParseTimeInput("2026-05-20 19:30");
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(2026, result.Value.Year);
|
|
Assert.Equal(5, result.Value.Month);
|
|
Assert.Equal(20, result.Value.Day);
|
|
Assert.Equal(19, result.Value.Hour);
|
|
Assert.Equal(30, result.Value.Minute);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseTimeInput_ShouldRejectPastDate()
|
|
{
|
|
var result = DiscordNewSessionHandler.ParseTimeInput("2020-01-01 00:00");
|
|
Assert.False(result.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseTimeInput_ShouldParseRussianDateFormat()
|
|
{
|
|
var result = DiscordNewSessionHandler.ParseTimeInput("20.05.2026 19:30");
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(2026, result.Value.Year);
|
|
Assert.Equal(5, result.Value.Month);
|
|
Assert.Equal(20, result.Value.Day);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseTimeInput_ShouldRejectInvalidFormat()
|
|
{
|
|
var result = DiscordNewSessionHandler.ParseTimeInput("not-a-date");
|
|
Assert.False(result.IsSuccess);
|
|
Assert.NotNull(result.Error);
|
|
}
|
|
|
|
// --- Source-level structural tests ---
|
|
|
|
[Fact]
|
|
public void Handler_ShouldExist()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
Assert.True(File.Exists(handlerPath), "DiscordNewSessionHandler should exist.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldUseDapperForDatabaseAccess()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.Contains("QueryAsync", source, StringComparison.Ordinal);
|
|
Assert.Contains("ExecuteAsync", source, StringComparison.Ordinal);
|
|
Assert.Contains("ExecuteScalarAsync", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldUseNpgsqlDataSource()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.Contains("NpgsqlDataSource", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldCheckPermissionsViaPermissionChecker()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.Contains("CanManageSchedule", source, StringComparison.Ordinal);
|
|
Assert.Contains("UnauthorizedAccessException", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldBePlatformNeutral()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.DoesNotContain("telegram_chat_id", source, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("telegram_id", source, StringComparison.Ordinal);
|
|
Assert.Contains("platform = 'Discord'", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldUseTransactions()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.Contains("BeginTransactionAsync", source, StringComparison.Ordinal);
|
|
Assert.Contains("CommitAsync", source, StringComparison.Ordinal);
|
|
Assert.Contains("RollbackAsync", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handler_ShouldRespectCancellationToken()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
|
var source = File.ReadAllText(handlerPath);
|
|
|
|
Assert.Contains("CancellationToken", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_ShouldRenderEmbedOnSuccess()
|
|
{
|
|
var repoRoot = GetRepoRoot();
|
|
var commandPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionCommand.cs");
|
|
var source = File.ReadAllText(commandPath);
|
|
|
|
Assert.Contains("DiscordSessionBatchRenderer.Render", source, StringComparison.Ordinal);
|
|
Assert.Contains("WithEmbeds", source, StringComparison.Ordinal);
|
|
}
|
|
}
|