mirror of
https://github.com/Stone-Red-Code/EchoHub.git
synced 2026-09-07 23:40:19 +02:00
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:
@@ -0,0 +1,294 @@
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Server.Irc;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests.Irc;
|
||||
|
||||
public class IrcMessageFormatterTests
|
||||
{
|
||||
private static MessageDto CreateTextMessage(string content, string sender = "alice",
|
||||
string channel = "general", List<EmbedDto>? embeds = null)
|
||||
{
|
||||
return new MessageDto(
|
||||
Guid.NewGuid(), content, sender, null, channel,
|
||||
MessageType.Text, null, null, DateTimeOffset.UtcNow, embeds);
|
||||
}
|
||||
|
||||
private static MessageDto CreateImageMessage(string asciiArt, string fileName = "image.png",
|
||||
string url = "https://example.com/image.png", string sender = "alice", string channel = "general")
|
||||
{
|
||||
return new MessageDto(
|
||||
Guid.NewGuid(), asciiArt, sender, null, channel,
|
||||
MessageType.Image, url, fileName, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static MessageDto CreateFileMessage(string fileName = "doc.pdf",
|
||||
string url = "https://example.com/doc.pdf", string sender = "alice", string channel = "general")
|
||||
{
|
||||
return new MessageDto(
|
||||
Guid.NewGuid(), "", sender, null, channel,
|
||||
MessageType.File, url, fileName, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static MessageDto CreateAudioMessage(string fileName = "song.mp3",
|
||||
string url = "https://example.com/song.mp3", string sender = "alice", string channel = "general")
|
||||
{
|
||||
return new MessageDto(
|
||||
Guid.NewGuid(), "", sender, null, channel,
|
||||
MessageType.Audio, url, fileName, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
// ── FormatMessage ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_TextMessage_FormatsAsPrivmsg()
|
||||
{
|
||||
var msg = CreateTextMessage("Hello world");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal(":alice!alice@echohub PRIVMSG #general :Hello world", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_TextMessage_IncludesChannelHash()
|
||||
{
|
||||
var msg = CreateTextMessage("test", channel: "random");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Contains("#random", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_TextWithEmbeds_AppendsEmbedLines()
|
||||
{
|
||||
var embeds = new List<EmbedDto>
|
||||
{
|
||||
new("Example Site", "Page Title", "A description of the page", null, "https://example.com")
|
||||
};
|
||||
var msg = CreateTextMessage("Check this: https://example.com", embeds: embeds);
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.True(lines.Count >= 2);
|
||||
Assert.Contains("Check this: https://example.com", lines[0]);
|
||||
// Embed header
|
||||
Assert.Contains("Example Site", lines[1]);
|
||||
Assert.Contains("Page Title", lines[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_EmbedWithDescription_IncludesDescription()
|
||||
{
|
||||
var embeds = new List<EmbedDto>
|
||||
{
|
||||
new("Site", "Title", "This is a description", null, "https://example.com")
|
||||
};
|
||||
var msg = CreateTextMessage("url", embeds: embeds);
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.True(lines.Count >= 3);
|
||||
Assert.Contains("This is a description", lines[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_EmbedWithLongDescription_Truncates()
|
||||
{
|
||||
var longDesc = new string('x', 300);
|
||||
var embeds = new List<EmbedDto>
|
||||
{
|
||||
new("Site", "Title", longDesc, null, "https://example.com")
|
||||
};
|
||||
var msg = CreateTextMessage("url", embeds: embeds);
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
var descLine = lines.First(l => l.Contains("xxx"));
|
||||
Assert.Contains("...", descLine);
|
||||
// Should be truncated to ~200 chars
|
||||
var descContent = descLine[(descLine.LastIndexOf(':') + 2)..]; // after ":│ "
|
||||
Assert.True(descContent.Length <= 210);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_ImageMessage_IncludesFileNameAndUrl()
|
||||
{
|
||||
var msg = CreateImageMessage("##\n##", "photo.jpg", "https://example.com/photo.jpg");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Contains(lines, l => l.Contains("[Image: photo.jpg]"));
|
||||
Assert.Contains(lines, l => l.Contains("Download: https://example.com/photo.jpg"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_ImageMessage_IncludesAsciiArt()
|
||||
{
|
||||
var msg = CreateImageMessage("line1\nline2");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Contains(lines, l => l.Contains("line1"));
|
||||
Assert.Contains(lines, l => l.Contains("line2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_ImageMessage_SkipsEmptyAsciiLines()
|
||||
{
|
||||
var msg = CreateImageMessage("line1\n\nline2");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
// Empty lines should be skipped
|
||||
var asciiLines = lines.Where(l => !l.Contains("[Image:") && !l.Contains("Download:")).ToList();
|
||||
Assert.Equal(2, asciiLines.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_FileMessage_FormatsCorrectly()
|
||||
{
|
||||
var msg = CreateFileMessage("report.pdf", "https://example.com/report.pdf");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Contains("[File: report.pdf]", lines[0]);
|
||||
Assert.Contains("https://example.com/report.pdf", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_AudioMessage_FormatsWithMusicNote()
|
||||
{
|
||||
var msg = CreateAudioMessage("track.mp3", "https://example.com/track.mp3");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Contains("\u266a", lines[0]); // ♪
|
||||
Assert.Contains("[Audio: track.mp3]", lines[0]);
|
||||
Assert.Contains("https://example.com/track.mp3", lines[0]);
|
||||
}
|
||||
|
||||
// ── SplitMessage ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_ShortMessage_ReturnsSingleChunk()
|
||||
{
|
||||
var chunks = IrcMessageFormatter.SplitMessage("Hello", 400);
|
||||
Assert.Single(chunks);
|
||||
Assert.Equal("Hello", chunks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_ExactlyAtLimit_ReturnsSingleChunk()
|
||||
{
|
||||
var msg = new string('a', 400);
|
||||
var chunks = IrcMessageFormatter.SplitMessage(msg, 400);
|
||||
Assert.Single(chunks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_LongMessage_SplitsAtWordBoundary()
|
||||
{
|
||||
// Create a message that's longer than 50 bytes
|
||||
var words = string.Join(" ", Enumerable.Repeat("hello", 20)); // 20 * 6 - 1 = 119 bytes
|
||||
var chunks = IrcMessageFormatter.SplitMessage(words, 50);
|
||||
|
||||
Assert.True(chunks.Count > 1);
|
||||
// Each chunk should be roughly <= 50 bytes
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
Assert.True(System.Text.Encoding.UTF8.GetByteCount(chunk) <= 55,
|
||||
$"Chunk too long: {chunk.Length} chars");
|
||||
}
|
||||
// Reassembled content should match original
|
||||
var reassembled = string.Join(" ", chunks);
|
||||
Assert.Equal(words, reassembled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_SingleLongWord_ForcedIntoOneChunk()
|
||||
{
|
||||
var longWord = new string('a', 500);
|
||||
var chunks = IrcMessageFormatter.SplitMessage(longWord, 400);
|
||||
// A single word can't be split at word boundaries, so it stays as one chunk
|
||||
Assert.Single(chunks);
|
||||
Assert.Equal(longWord, chunks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_EmptyString_ReturnsSingleEmpty()
|
||||
{
|
||||
var chunks = IrcMessageFormatter.SplitMessage("", 400);
|
||||
Assert.Single(chunks);
|
||||
Assert.Equal("", chunks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
|
||||
{
|
||||
// Japanese text: each char is 3 bytes in UTF-8
|
||||
var text = string.Join(" ", Enumerable.Repeat("\u3042\u3044\u3046", 50));
|
||||
var chunks = IrcMessageFormatter.SplitMessage(text, 100);
|
||||
|
||||
Assert.True(chunks.Count > 1);
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
Assert.True(System.Text.Encoding.UTF8.GetByteCount(chunk) <= 110,
|
||||
$"Chunk too long in bytes: {System.Text.Encoding.UTF8.GetByteCount(chunk)}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── ColorTagsToAnsi ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_NoTags_ReturnsUnchanged()
|
||||
{
|
||||
Assert.Equal("Hello world", IrcMessageFormatter.ColorTagsToAnsi("Hello world"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_ForegroundTag_ConvertsToAnsi()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{F:FF0000}Red text");
|
||||
Assert.Equal("\x1b[38;2;255;0;0mRed text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_BackgroundTag_ConvertsToAnsi()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{B:00FF00}Green bg");
|
||||
Assert.Equal("\x1b[48;2;0;255;0mGreen bg", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_ResetTag_ConvertsToReset()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{F:FF0000}Red{X} Normal");
|
||||
Assert.Equal("\x1b[38;2;255;0;0mRed\x1b[0m Normal", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_MultipleTags_ConvertsAll()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{F:FF0000}Red {F:0000FF}Blue{X}");
|
||||
Assert.Contains("\x1b[38;2;255;0;0m", result);
|
||||
Assert.Contains("\x1b[38;2;0;0;255m", result);
|
||||
Assert.Contains("\x1b[0m", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_LowercaseHex_ConvertsCorrectly()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{F:ff8800}text");
|
||||
Assert.Equal("\x1b[38;2;255;136;0mtext", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_NoBraces_SkipsProcessing()
|
||||
{
|
||||
var text = "plain text without braces";
|
||||
Assert.Equal(text, IrcMessageFormatter.ColorTagsToAnsi(text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_ExistingAnsiCodes_PreservesUnchanged()
|
||||
{
|
||||
var text = "\x1b[31mAlready colored\x1b[0m";
|
||||
Assert.Equal(text, IrcMessageFormatter.ColorTagsToAnsi(text));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user