Files
EchoHub/src/EchoHub.Tests/Irc/IrcClientConnectionTests.cs
T
HueByte c0d38015ff 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.
2026-02-21 11:25:34 +01:00

214 lines
6.6 KiB
C#

using EchoHub.Server.Irc;
using Xunit;
namespace EchoHub.Tests.Irc;
public class IrcClientConnectionTests
{
// ── Connection identity ──────────────────────────────────────────────
[Fact]
public void ConnectionId_StartsWithIrcPrefix()
{
var (conn, _) = TestIrcConnectionFactory.Create();
Assert.StartsWith("irc-", conn.ConnectionId);
}
[Fact]
public void ConnectionId_IsUnique()
{
var (conn1, _) = TestIrcConnectionFactory.Create();
var (conn2, _) = TestIrcConnectionFactory.Create();
Assert.NotEqual(conn1.ConnectionId, conn2.ConnectionId);
}
// ── Hostmask ─────────────────────────────────────────────────────────
[Fact]
public void Hostmask_WithNicknameAndUsername_FormatsCorrectly()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.Nickname = "alice";
conn.Username = "alice_user";
Assert.Equal("alice!alice_user@echohub", conn.Hostmask);
}
[Fact]
public void Hostmask_WithoutUsername_FallsBackToNickname()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.Nickname = "alice";
Assert.Equal("alice!alice@echohub", conn.Hostmask);
}
// ── I/O ──────────────────────────────────────────────────────────────
[Fact]
public async Task ReadLineAsync_ReturnsInputLines()
{
var (conn, _) = TestIrcConnectionFactory.Create("PING", "PONG");
var line1 = await conn.ReadLineAsync(CancellationToken.None);
var line2 = await conn.ReadLineAsync(CancellationToken.None);
Assert.Equal("PING", line1);
Assert.Equal("PONG", line2);
}
[Fact]
public async Task ReadLineAsync_EndOfStream_ReturnsNull()
{
var (conn, _) = TestIrcConnectionFactory.Create("PING");
await conn.ReadLineAsync(CancellationToken.None); // consume "PING"
var result = await conn.ReadLineAsync(CancellationToken.None);
Assert.Null(result);
}
[Fact]
public async Task SendAsync_WritesToOutput()
{
var (conn, stream) = TestIrcConnectionFactory.Create();
await conn.SendAsync(":server 001 alice :Welcome!");
var output = stream.GetOutputLines();
Assert.Single(output);
Assert.Equal(":server 001 alice :Welcome!", output[0]);
}
[Fact]
public async Task SendAsync_MultipleLines_AllCaptured()
{
var (conn, stream) = TestIrcConnectionFactory.Create();
await conn.SendAsync("line1");
await conn.SendAsync("line2");
await conn.SendAsync("line3");
var output = stream.GetOutputLines();
Assert.Equal(3, output.Count);
}
[Fact]
public async Task SendNumericAsync_FormatsCorrectly()
{
var (conn, stream) = TestIrcConnectionFactory.Create();
conn.Nickname = "alice";
await conn.SendNumericAsync("testserver", "001", ":Welcome!");
var output = stream.GetOutputLines();
Assert.Single(output);
Assert.Equal(":testserver 001 alice :Welcome!", output[0]);
}
[Fact]
public async Task SendNumericAsync_NoNickname_UsesStar()
{
var (conn, stream) = TestIrcConnectionFactory.Create();
await conn.SendNumericAsync("testserver", "451", ":Not registered");
var output = stream.GetOutputLines();
Assert.Contains("*", output[0]);
}
// ── Thread-safe channel operations ───────────────────────────────────
[Fact]
public void JoinChannel_AddsChannel()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.JoinChannel("general");
Assert.True(conn.IsInChannel("general"));
}
[Fact]
public void LeaveChannel_RemovesChannel()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.JoinChannel("general");
conn.LeaveChannel("general");
Assert.False(conn.IsInChannel("general"));
}
[Fact]
public void IsInChannel_CaseInsensitive()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.JoinChannel("General");
Assert.True(conn.IsInChannel("general"));
Assert.True(conn.IsInChannel("GENERAL"));
}
[Fact]
public void GetJoinedChannels_ReturnsSnapshot()
{
var (conn, _) = TestIrcConnectionFactory.Create();
conn.JoinChannel("general");
conn.JoinChannel("random");
var channels = conn.GetJoinedChannels();
Assert.Equal(2, channels.Count);
Assert.Contains("general", channels);
Assert.Contains("random", channels);
// Modifying the returned list shouldn't affect the connection state
channels.Clear();
Assert.True(conn.IsInChannel("general"));
}
[Fact]
public async Task JoinedChannels_ConcurrentAccess_DoesNotThrow()
{
var (conn, _) = TestIrcConnectionFactory.Create();
// Simulate concurrent reads and writes (broadcaster reads while handler writes)
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var writerTask = Task.Run(async () =>
{
for (int i = 0; i < 1000 && !cts.IsCancellationRequested; i++)
{
conn.JoinChannel($"channel-{i}");
await Task.Yield();
if (i % 3 == 0) conn.LeaveChannel($"channel-{i}");
}
}, cts.Token);
var readerTask = Task.Run(async () =>
{
for (int i = 0; i < 1000 && !cts.IsCancellationRequested; i++)
{
_ = conn.IsInChannel($"channel-{i}");
_ = conn.GetJoinedChannels();
await Task.Yield();
}
}, cts.Token);
// Should complete without exceptions
await Task.WhenAll(writerTask, readerTask);
}
// ── Default state ────────────────────────────────────────────────────
[Fact]
public void NewConnection_IsNotRegistered()
{
var (conn, _) = TestIrcConnectionFactory.Create();
Assert.False(conn.IsRegistered);
Assert.False(conn.IsAuthenticated);
Assert.Null(conn.Nickname);
Assert.Null(conn.Username);
Assert.Null(conn.UserId);
Assert.Null(conn.AwayMessage);
}
}