Add comprehensive unit tests for IRC command handling and message formatting

- Implemented IrcCommandHandlerTests to cover various IRC commands including PING, JOIN, PART, and authentication scenarios.
- Added IrcMessageFormatterTests to validate message formatting for different message types such as text, images, files, and audio.
- Created IrcMessageTests to ensure correct parsing of IRC messages and handling of various command formats.
- Introduced TestHelpers to facilitate testing with mock connections and streams, including a FakeChatService and FakeEncryptionService for simulating chat behavior.
This commit is contained in:
HueByte
2026-02-21 11:25:34 +01:00
parent 3bc1446d1c
commit c0d38015ff
14 changed files with 1907 additions and 23 deletions
@@ -27,14 +27,20 @@ public sealed class IrcClientConnection : IAsyncDisposable
public bool IsSasl { get; set; }
public bool CapNegotiating { get; set; }
// Channel state
public HashSet<string> JoinedChannels { get; } = new(StringComparer.OrdinalIgnoreCase);
// Channel state — thread-safe: written by command handler, read by broadcaster threads
private readonly HashSet<string> _joinedChannels = new(StringComparer.OrdinalIgnoreCase);
private readonly object _channelLock = new();
// Away state
public string? AwayMessage { get; set; }
public string Hostmask => $"{Nickname}!{Username ?? Nickname}@echohub";
public void JoinChannel(string channel) { lock (_channelLock) _joinedChannels.Add(channel); }
public void LeaveChannel(string channel) { lock (_channelLock) _joinedChannels.Remove(channel); }
public bool IsInChannel(string channel) { lock (_channelLock) return _joinedChannels.Contains(channel); }
public List<string> GetJoinedChannels() { lock (_channelLock) return [.. _joinedChannels]; }
public IrcClientConnection(TcpClient tcpClient, Stream stream)
{
_tcpClient = tcpClient;
+20 -16
View File
@@ -12,6 +12,7 @@ public sealed class IrcCommandHandler
private readonly IrcClientConnection _conn;
private readonly IrcOptions _options;
private readonly IChatService _chatService;
private readonly IMessageEncryptionService _encryption;
private readonly ILogger _logger;
private string ServerName => _options.ServerName;
@@ -20,11 +21,13 @@ public sealed class IrcCommandHandler
IrcClientConnection conn,
IrcOptions options,
IChatService chatService,
IMessageEncryptionService encryption,
ILogger logger)
{
_conn = conn;
_options = options;
_chatService = chatService;
_encryption = encryption;
_logger = logger;
}
@@ -319,7 +322,7 @@ public sealed class IrcCommandHandler
private async Task HandleJoinAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1)
{
@@ -350,7 +353,7 @@ public sealed class IrcCommandHandler
continue;
}
_conn.JoinedChannels.Add(channelName);
_conn.JoinChannel(channelName);
// Confirm JOIN to the client
await _conn.SendAsync($":{_conn.Hostmask} JOIN #{channelName}");
@@ -361,10 +364,11 @@ public sealed class IrcCommandHandler
// Send NAMES list
await SendNamesReplyAsync(channelName);
// Replay history
// Replay history (decrypt — history is encrypted for SignalR transport)
foreach (var m in history)
{
var lines = IrcMessageFormatter.FormatMessage(m);
var decrypted = m with { Content = _encryption.Decrypt(m.Content) };
var lines = IrcMessageFormatter.FormatMessage(decrypted);
foreach (var line in lines)
await _conn.SendAsync(line);
}
@@ -373,7 +377,7 @@ public sealed class IrcCommandHandler
private async Task HandlePartAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var channels = msg.Parameters[0].Split(',', StringSplitOptions.RemoveEmptyEntries);
@@ -385,7 +389,7 @@ public sealed class IrcCommandHandler
if (channelName is null) continue;
await _chatService.LeaveChannelAsync(_conn.ConnectionId, _conn.Nickname!, channelName);
_conn.JoinedChannels.Remove(channelName);
_conn.LeaveChannel(channelName);
await _conn.SendAsync($":{_conn.Hostmask} PART #{channelName}" +
(partMessage is not null ? $" :{partMessage}" : ""));
@@ -394,7 +398,7 @@ public sealed class IrcCommandHandler
private async Task HandlePrivmsgAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 2)
{
@@ -436,7 +440,7 @@ public sealed class IrcCommandHandler
private async Task HandleNamesAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var channelName = IrcToEchoHubChannel(msg.Parameters[0]);
@@ -458,7 +462,7 @@ public sealed class IrcCommandHandler
private async Task HandleTopicAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var channelName = IrcToEchoHubChannel(msg.Parameters[0]);
@@ -495,7 +499,7 @@ public sealed class IrcCommandHandler
private async Task HandleWhoAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var channelName = IrcToEchoHubChannel(msg.Parameters[0]);
@@ -516,7 +520,7 @@ public sealed class IrcCommandHandler
private async Task HandleWhoisAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var nick = msg.Parameters[^1].ToLowerInvariant();
@@ -559,7 +563,7 @@ public sealed class IrcCommandHandler
private async Task HandleAwayAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count > 0 && !string.IsNullOrWhiteSpace(msg.Parameters[0]))
{
@@ -581,7 +585,7 @@ public sealed class IrcCommandHandler
private async Task HandleListAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
var channels = await _chatService.GetChannelListAsync();
@@ -597,7 +601,7 @@ public sealed class IrcCommandHandler
private async Task HandleModeAsync(IrcMessage msg)
{
if (!RequireRegistered()) return;
if (!await RequireRegisteredAsync()) return;
if (msg.Parameters.Count < 1) return;
var target = msg.Parameters[0];
@@ -621,11 +625,11 @@ public sealed class IrcCommandHandler
// ── Helpers ──────────────────────────────────────────────────────────────
private bool RequireRegistered()
private async Task<bool> RequireRegisteredAsync()
{
if (_conn.IsRegistered) return true;
_ = _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_NOTREGISTERED,
await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_NOTREGISTERED,
":You have not registered");
return false;
}
+4 -3
View File
@@ -34,7 +34,7 @@ public sealed class IrcGatewayService : BackgroundService
public IEnumerable<IrcClientConnection> GetConnectionsInChannel(string channelName)
{
return _connections.Values
.Where(c => c.IsAuthenticated && c.JoinedChannels.Contains(channelName));
.Where(c => c.IsAuthenticated && c.IsInChannel(channelName));
}
public IEnumerable<IrcClientConnection> GetAllConnections()
@@ -120,8 +120,9 @@ public sealed class IrcGatewayService : BackgroundService
try
{
chatService = _services.GetRequiredService<IChatService>();
var encryption = _services.GetRequiredService<IMessageEncryptionService>();
var handler = new IrcCommandHandler(
connection, _options, chatService, _logger);
connection, _options, chatService, encryption, _logger);
await handler.RunAsync(ct);
}
@@ -133,7 +134,7 @@ public sealed class IrcGatewayService : BackgroundService
{
if (connection.IsAuthenticated)
{
foreach (var ch in connection.JoinedChannels.ToList())
foreach (var ch in connection.GetJoinedChannels())
{
if (chatService is null) break;
await chatService.LeaveChannelAsync(