mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
Add unit tests for various services and functionalities
- Implement tests for ChatLine utility methods including color tag detection and stripping. - Create comprehensive tests for CommandHandler to validate command handling and status updates. - Add tests for DataMigrationService to ensure correct ANSI to color tag conversion. - Enhance FileValidationHelperTests with audio file validation tests. - Introduce ImageToAsciiServiceTests to verify image dimension retrieval. - Develop IrcMessageFormatterTests for message formatting and color tag conversion. - Add JwtTokenServiceTests to validate JWT token generation and hashing. - Implement LinkEmbedServiceTests for URL extraction and Open Graph tag parsing. - Update EchoHub.Tests.csproj to include necessary project references.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using EchoHub.Client.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for static string-utility methods on ChatLine.
|
||||
/// Note: Tests that construct ChatLine/ChatListSource or use Terminal.Gui types
|
||||
/// (Attribute, Color) are excluded because Terminal.Gui's module initializer
|
||||
/// requires a display driver which is unavailable in CI/test environments.
|
||||
/// </summary>
|
||||
public class ChatLineTests
|
||||
{
|
||||
// ── HasColorTags ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_ForegroundTag_ReturnsTrue()
|
||||
{
|
||||
Assert.True(ChatLine.HasColorTags("Hello {F:FF0000}world"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_BackgroundTag_ReturnsTrue()
|
||||
{
|
||||
Assert.True(ChatLine.HasColorTags("Hello {B:00FF00}world"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_ResetTag_ReturnsTrue()
|
||||
{
|
||||
Assert.True(ChatLine.HasColorTags("Hello{X}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_NoTags_ReturnsFalse()
|
||||
{
|
||||
Assert.False(ChatLine.HasColorTags("Hello world"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_PartialTag_ReturnsFalse()
|
||||
{
|
||||
// {Z:...} is not a valid tag (only F or B)
|
||||
Assert.False(ChatLine.HasColorTags("Hello {Z:000000}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasColorTags_EmptyString_ReturnsFalse()
|
||||
{
|
||||
Assert.False(ChatLine.HasColorTags(""));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{F:AABBCC}text")]
|
||||
[InlineData("prefix{B:112233}suffix")]
|
||||
[InlineData("a{X}b")]
|
||||
[InlineData("{F:000000}{B:FFFFFF}{X}")]
|
||||
public void HasColorTags_VariousValidTags_ReturnsTrue(string input)
|
||||
{
|
||||
Assert.True(ChatLine.HasColorTags(input));
|
||||
}
|
||||
|
||||
// ── StripColorTags ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_RemovesAllTags()
|
||||
{
|
||||
var result = ChatLine.StripColorTags("{F:FF0000}red{B:00FF00}green{X}");
|
||||
Assert.Equal("redgreen", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_NoTags_ReturnsOriginal()
|
||||
{
|
||||
var result = ChatLine.StripColorTags("plain text");
|
||||
Assert.Equal("plain text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_OnlyTags_ReturnsEmpty()
|
||||
{
|
||||
var result = ChatLine.StripColorTags("{F:AABBCC}{B:112233}{X}");
|
||||
Assert.Equal("", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_MixedContent_KeepsText()
|
||||
{
|
||||
var result = ChatLine.StripColorTags("before{F:FF0000}middle{X}after");
|
||||
Assert.Equal("beforemiddleafter", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_MultipleConsecutiveTags_AllStripped()
|
||||
{
|
||||
var result = ChatLine.StripColorTags("{F:FF0000}{B:00FF00}{X}{F:0000FF}text{X}");
|
||||
Assert.Equal("text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripColorTags_PreservesNonTagBraces()
|
||||
{
|
||||
// {Hello} is not a valid tag and should be preserved
|
||||
var result = ChatLine.StripColorTags("{Hello} world");
|
||||
Assert.Equal("{Hello} world", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
using EchoHub.Client.Commands;
|
||||
using EchoHub.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class CommandHandlerTests
|
||||
{
|
||||
private CommandHandler CreateHandler() => new();
|
||||
|
||||
// ── IsCommand ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsCommand_StartsWithSlash_ReturnsTrue()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
Assert.True(handler.IsCommand("/help"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCommand_NoSlash_ReturnsFalse()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
Assert.False(handler.IsCommand("hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCommand_EmptyString_ReturnsFalse()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
Assert.False(handler.IsCommand(""));
|
||||
}
|
||||
|
||||
// ── HandleAsync — not a command ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_NotCommand_ReturnsFalse()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("hello world");
|
||||
|
||||
Assert.False(result.Handled);
|
||||
}
|
||||
|
||||
// ── HandleAsync — unknown command ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_UnknownCommand_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/doesnotexist");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Unknown command", result.Message);
|
||||
}
|
||||
|
||||
// ── /status ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_StatusOnline_SetsOnlineStatus()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
UserStatus? capturedStatus = null;
|
||||
handler.OnSetStatus += (status, msg) => { capturedStatus = status; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/status online");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(UserStatus.Online, capturedStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_StatusAway_SetsAwayStatus()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
UserStatus? capturedStatus = null;
|
||||
handler.OnSetStatus += (status, msg) => { capturedStatus = status; return Task.CompletedTask; };
|
||||
|
||||
await handler.HandleAsync("/status away");
|
||||
Assert.Equal(UserStatus.Away, capturedStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_StatusCustomMessage_SetsStatusMessage()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedMessage = null;
|
||||
handler.OnSetStatus += (status, msg) => { capturedMessage = msg; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/status brb lunch");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.Contains("brb lunch", result.Message);
|
||||
Assert.Equal("brb lunch", capturedMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_StatusNoArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/status");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Usage", result.Message);
|
||||
}
|
||||
|
||||
// ── /nick ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Nick_SetsDisplayName()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedNick = null;
|
||||
handler.OnSetNick += nick => { capturedNick = nick; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/nick Bob Smith");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.Equal("Bob Smith", capturedNick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Nick_EmptyArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/nick");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Usage", result.Message);
|
||||
}
|
||||
|
||||
// ── /color ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Color_ValidHex_Succeeds()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedColor = null;
|
||||
handler.OnSetColor += color => { capturedColor = color; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/color #FF5733");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("#FF5733", capturedColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Color_WithoutHash_AddsHash()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedColor = null;
|
||||
handler.OnSetColor += color => { capturedColor = color; return Task.CompletedTask; };
|
||||
|
||||
await handler.HandleAsync("/color FF5733");
|
||||
Assert.Equal("#FF5733", capturedColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Color_InvalidHex_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/color #ZZZZZZ");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Invalid color", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Color_TooShort_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/color #FFF");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Color_NoArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/color");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
// ── /join ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Join_StripsHashPrefix()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedChannel = null;
|
||||
handler.OnJoinChannel += ch => { capturedChannel = ch; return Task.CompletedTask; };
|
||||
|
||||
await handler.HandleAsync("/join #random");
|
||||
Assert.Equal("random", capturedChannel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Join_NoHash_PassedDirectly()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedChannel = null;
|
||||
handler.OnJoinChannel += ch => { capturedChannel = ch; return Task.CompletedTask; };
|
||||
|
||||
await handler.HandleAsync("/join random");
|
||||
Assert.Equal("random", capturedChannel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Join_NoArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/join");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
// ── /kick ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Kick_WithReason_ParsesUsernameAndReason()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedUser = null;
|
||||
string? capturedReason = null;
|
||||
handler.OnKickUser += (user, reason) =>
|
||||
{
|
||||
capturedUser = user;
|
||||
capturedReason = reason;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await handler.HandleAsync("/kick baduser being rude");
|
||||
Assert.Equal("baduser", capturedUser);
|
||||
Assert.Equal("being rude", capturedReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Kick_WithoutReason_NullReason()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedReason = "initial";
|
||||
handler.OnKickUser += (user, reason) =>
|
||||
{
|
||||
capturedReason = reason;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await handler.HandleAsync("/kick baduser");
|
||||
Assert.Null(capturedReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Kick_NoArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/kick");
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
// ── /mute ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Mute_WithDuration_ParsesDuration()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
int? capturedDuration = null;
|
||||
handler.OnMuteUser += (user, duration) =>
|
||||
{
|
||||
capturedDuration = duration;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await handler.HandleAsync("/mute alice 30");
|
||||
Assert.Equal(30, capturedDuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Mute_WithoutDuration_NullDuration()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
int? capturedDuration = -1;
|
||||
handler.OnMuteUser += (user, duration) =>
|
||||
{
|
||||
capturedDuration = duration;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await handler.HandleAsync("/mute alice");
|
||||
Assert.Null(capturedDuration);
|
||||
}
|
||||
|
||||
// ── /role ─────────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("admin")]
|
||||
[InlineData("mod")]
|
||||
[InlineData("member")]
|
||||
public async Task HandleAsync_Role_ValidRole_Succeeds(string role)
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedRole = null;
|
||||
handler.OnAssignRole += (user, r) => { capturedRole = r; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync($"/role alice {role}");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(role, capturedRole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Role_InvalidRole_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/role alice superadmin");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Invalid role", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Role_MissingRole_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/role alice");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
// ── /send ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Send_NoArgs_ReturnsUsageError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/send");
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Contains("Usage", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Send_UrlInput_RecognizedAsUrl()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedTarget = null;
|
||||
handler.OnSendFile += (target, size) => { capturedTarget = target; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/send https://example.com/image.png");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://example.com/image.png", capturedTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Send_UrlWithSizeFlag_ExtractsSizeCorrectly()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedSize = null;
|
||||
handler.OnSendFile += (target, size) => { capturedSize = size; return Task.CompletedTask; };
|
||||
|
||||
await handler.HandleAsync("/send https://example.com/photo.jpg -s");
|
||||
Assert.Equal("s", capturedSize);
|
||||
}
|
||||
|
||||
// ── /help ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Help_ReturnsHelpText()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/help");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.Contains("Available commands", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_QuestionMark_ReturnsHelp()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/?");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.Contains("Available commands", result.Message);
|
||||
}
|
||||
|
||||
// ── /ban ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Ban_WithReason_ParsesUsernameAndReason()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedUser = null;
|
||||
string? capturedReason = null;
|
||||
handler.OnBanUser += (user, reason) =>
|
||||
{
|
||||
capturedUser = user;
|
||||
capturedReason = reason;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await handler.HandleAsync("/ban troll spamming links");
|
||||
Assert.Equal("troll", capturedUser);
|
||||
Assert.Equal("spamming links", capturedReason);
|
||||
}
|
||||
|
||||
// ── /topic ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Topic_SetsTopic()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
string? capturedTopic = null;
|
||||
handler.OnSetTopic += topic => { capturedTopic = topic; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/topic Welcome to our channel!");
|
||||
|
||||
Assert.True(result.Handled);
|
||||
Assert.Equal("Welcome to our channel!", capturedTopic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Topic_NoArgs_ReturnsError()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var result = await handler.HandleAsync("/topic");
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
|
||||
// ── /quit and /exit ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Quit_Handled()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var quitCalled = false;
|
||||
handler.OnQuit += () => { quitCalled = true; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/quit");
|
||||
Assert.True(result.Handled);
|
||||
Assert.True(quitCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_Exit_Handled()
|
||||
{
|
||||
var handler = CreateHandler();
|
||||
var quitCalled = false;
|
||||
handler.OnQuit += () => { quitCalled = true; return Task.CompletedTask; };
|
||||
|
||||
var result = await handler.HandleAsync("/exit");
|
||||
Assert.True(result.Handled);
|
||||
Assert.True(quitCalled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using EchoHub.Server.Setup;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class DataMigrationServiceTests
|
||||
{
|
||||
// ── AnsiToColorTags ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_ForegroundEscape_ConvertedToColorTag()
|
||||
{
|
||||
var ansi = "\x1b[38;2;255;0;0mred text";
|
||||
var result = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal("{F:FF0000}red text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_BackgroundEscape_ConvertedToColorTag()
|
||||
{
|
||||
var ansi = "\x1b[48;2;0;255;0mgreen bg";
|
||||
var result = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal("{B:00FF00}green bg", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_ResetEscape_ConvertedToResetTag()
|
||||
{
|
||||
var ansi = "\x1b[0m";
|
||||
var result = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal("{X}", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_NoEscapes_ReturnsUnchanged()
|
||||
{
|
||||
var text = "Hello, world!";
|
||||
var result = DataMigrationService.AnsiToColorTags(text);
|
||||
|
||||
Assert.Equal("Hello, world!", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_MixedContent_ConvertsEscapesOnly()
|
||||
{
|
||||
var ansi = "before\x1b[38;2;100;200;50mtextafter";
|
||||
var result = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal("before{F:64C832}textafter", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_MultipleTags_AllConverted()
|
||||
{
|
||||
var ansi = "\x1b[38;2;255;0;0mred\x1b[48;2;0;0;255mblue bg\x1b[0mreset";
|
||||
var result = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal("{F:FF0000}red{B:0000FF}blue bg{X}reset", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnsiToColorTags_RoundTrip_WithColorTagsToAnsi()
|
||||
{
|
||||
// AnsiToColorTags and IrcMessageFormatter.ColorTagsToAnsi should be inverses
|
||||
var original = "{F:FF0000}red{B:00FF00}green{X}";
|
||||
var ansi = EchoHub.Server.Irc.IrcMessageFormatter.ColorTagsToAnsi(original);
|
||||
var backToTags = DataMigrationService.AnsiToColorTags(ansi);
|
||||
|
||||
Assert.Equal(original, backToTags);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\EchoHub.Client\EchoHub.Client.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Server\EchoHub.Server.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Server.Irc\EchoHub.Server.Irc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -53,4 +53,48 @@ public class FileValidationHelperTests
|
||||
FileValidationHelper.IsValidImage(stream);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
// ── IsAudioFile tests ─────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("song.mp3")]
|
||||
[InlineData("track.wav")]
|
||||
[InlineData("audio.ogg")]
|
||||
[InlineData("music.flac")]
|
||||
[InlineData("clip.aac")]
|
||||
[InlineData("podcast.m4a")]
|
||||
[InlineData("old.wma")]
|
||||
public void IsAudioFile_SupportedExtensions_ReturnsTrue(string fileName)
|
||||
{
|
||||
Assert.True(FileValidationHelper.IsAudioFile(fileName));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("song.MP3")]
|
||||
[InlineData("track.Wav")]
|
||||
[InlineData("audio.OGG")]
|
||||
[InlineData("music.FLAC")]
|
||||
public void IsAudioFile_CaseInsensitive_ReturnsTrue(string fileName)
|
||||
{
|
||||
Assert.True(FileValidationHelper.IsAudioFile(fileName));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("document.txt")]
|
||||
[InlineData("report.pdf")]
|
||||
[InlineData("app.exe")]
|
||||
[InlineData("photo.jpg")]
|
||||
[InlineData("image.png")]
|
||||
public void IsAudioFile_NonAudioExtension_ReturnsFalse(string fileName)
|
||||
{
|
||||
Assert.False(FileValidationHelper.IsAudioFile(fileName));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("noextension")]
|
||||
public void IsAudioFile_EmptyOrNoExtension_ReturnsFalse(string fileName)
|
||||
{
|
||||
Assert.False(FileValidationHelper.IsAudioFile(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using EchoHub.Core.Constants;
|
||||
using EchoHub.Server.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class ImageToAsciiServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDimensions_Small_Returns40x40()
|
||||
{
|
||||
var (w, h) = ImageToAsciiService.GetDimensions("s");
|
||||
Assert.Equal(40, w);
|
||||
Assert.Equal(40, h);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDimensions_Large_Returns120x120()
|
||||
{
|
||||
var (w, h) = ImageToAsciiService.GetDimensions("l");
|
||||
Assert.Equal(120, w);
|
||||
Assert.Equal(120, h);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDimensions_Default_Returns80x80()
|
||||
{
|
||||
var (w, h) = ImageToAsciiService.GetDimensions("m");
|
||||
Assert.Equal(HubConstants.AsciiArtWidth, w);
|
||||
Assert.Equal(HubConstants.AsciiArtHeightHalfBlock, h);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDimensions_Null_ReturnsDefault()
|
||||
{
|
||||
var (w, h) = ImageToAsciiService.GetDimensions(null);
|
||||
Assert.Equal(HubConstants.AsciiArtWidth, w);
|
||||
Assert.Equal(HubConstants.AsciiArtHeightHalfBlock, h);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDimensions_CaseInsensitive()
|
||||
{
|
||||
var (w1, h1) = ImageToAsciiService.GetDimensions("S");
|
||||
var (w2, h2) = ImageToAsciiService.GetDimensions("s");
|
||||
Assert.Equal(w1, w2);
|
||||
Assert.Equal(h1, h2);
|
||||
|
||||
var (w3, h3) = ImageToAsciiService.GetDimensions("L");
|
||||
var (w4, h4) = ImageToAsciiService.GetDimensions("l");
|
||||
Assert.Equal(w3, w4);
|
||||
Assert.Equal(h3, h4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDimensions_UnknownSize_ReturnsDefault()
|
||||
{
|
||||
var (w, h) = ImageToAsciiService.GetDimensions("xl");
|
||||
Assert.Equal(HubConstants.AsciiArtWidth, w);
|
||||
Assert.Equal(HubConstants.AsciiArtHeightHalfBlock, h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Server.Irc;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class IrcMessageFormatterTests
|
||||
{
|
||||
private static MessageDto CreateMessage(
|
||||
MessageType type = MessageType.Text,
|
||||
string content = "hello",
|
||||
string sender = "alice",
|
||||
string channel = "general",
|
||||
string? attachmentUrl = null,
|
||||
string? attachmentFileName = null,
|
||||
List<EmbedDto>? embeds = null) => new(
|
||||
Id: Guid.NewGuid(),
|
||||
Content: content,
|
||||
SenderUsername: sender,
|
||||
SenderNicknameColor: null,
|
||||
ChannelName: channel,
|
||||
Type: type,
|
||||
AttachmentUrl: attachmentUrl,
|
||||
AttachmentFileName: attachmentFileName,
|
||||
SentAt: DateTimeOffset.UtcNow,
|
||||
Embeds: embeds);
|
||||
|
||||
// ── FormatMessage ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_TextMessage_FormatsAsPRIVMSG()
|
||||
{
|
||||
var msg = CreateMessage(content: "Hello world");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Contains("PRIVMSG #general :Hello world", lines[0]);
|
||||
Assert.StartsWith(":alice!alice@echohub", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_TextMessage_WithEmbeds_AppendsEmbedLines()
|
||||
{
|
||||
var embeds = new List<EmbedDto>
|
||||
{
|
||||
new("GitHub", "Repo Title", "A description", null, "https://github.com/test")
|
||||
};
|
||||
var msg = CreateMessage(content: "check this out https://github.com/test", embeds: embeds);
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.True(lines.Count >= 2);
|
||||
Assert.Contains("PRIVMSG #general :check this out", lines[0]);
|
||||
// Embed lines contain the Unicode pipe char and site/title
|
||||
Assert.Contains("GitHub", lines[1]);
|
||||
Assert.Contains("Repo Title", lines[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_ImageMessage_IncludesImageTagAndDownloadUrl()
|
||||
{
|
||||
var msg = CreateMessage(
|
||||
type: MessageType.Image,
|
||||
content: "{F:FF0000}\u2588{X}",
|
||||
attachmentUrl: "/api/files/abc",
|
||||
attachmentFileName: "photo.png");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.True(lines.Count >= 2);
|
||||
Assert.Contains("[Image: photo.png]", lines[0]);
|
||||
Assert.Contains("Download: /api/files/abc", lines[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_FileMessage_IncludesFileTag()
|
||||
{
|
||||
var msg = CreateMessage(
|
||||
type: MessageType.File,
|
||||
content: "report.pdf",
|
||||
attachmentUrl: "/api/files/xyz",
|
||||
attachmentFileName: "report.pdf");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Contains("[File: report.pdf]", lines[0]);
|
||||
Assert.Contains("/api/files/xyz", lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_AudioMessage_IncludesMusicNoteAndAudioTag()
|
||||
{
|
||||
var msg = CreateMessage(
|
||||
type: MessageType.Audio,
|
||||
content: "song.mp3",
|
||||
attachmentUrl: "/api/files/def",
|
||||
attachmentFileName: "song.mp3");
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Contains("\u266a", lines[0]); // ♪
|
||||
Assert.Contains("[Audio: song.mp3]", lines[0]);
|
||||
Assert.Contains("/api/files/def", lines[0]);
|
||||
}
|
||||
|
||||
// ── ColorTagsToAnsi ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_ForegroundTag_ConvertsToAnsiEscape()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{F:FF0000}text");
|
||||
Assert.Contains("\x1b[38;2;255;0;0m", result);
|
||||
Assert.Contains("text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_BackgroundTag_ConvertsToAnsiEscape()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{B:00FF00}text");
|
||||
Assert.Contains("\x1b[48;2;0;255;0m", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_ResetTag_ConvertsToAnsiReset()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("{X}");
|
||||
Assert.Equal("\x1b[0m", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTagsToAnsi_NoTags_ReturnsUnchanged()
|
||||
{
|
||||
var result = IrcMessageFormatter.ColorTagsToAnsi("plain text");
|
||||
Assert.Equal("plain text", 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);
|
||||
Assert.Contains("red", result);
|
||||
Assert.Contains("blue", result);
|
||||
}
|
||||
|
||||
// ── SplitMessage ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_ShortMessage_ReturnsSingleChunk()
|
||||
{
|
||||
var result = IrcMessageFormatter.SplitMessage("Hello", 400);
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Hello", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_LongMessage_SplitsAtWordBoundary()
|
||||
{
|
||||
var words = string.Join(" ", Enumerable.Repeat("word", 200));
|
||||
var result = IrcMessageFormatter.SplitMessage(words, 50);
|
||||
|
||||
Assert.True(result.Count > 1);
|
||||
foreach (var chunk in result)
|
||||
Assert.True(System.Text.Encoding.UTF8.GetByteCount(chunk) <= 50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_EmptyMessage_ReturnsSingleEmptyChunk()
|
||||
{
|
||||
var result = IrcMessageFormatter.SplitMessage("", 400);
|
||||
Assert.Single(result);
|
||||
Assert.Equal("", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_SingleLongWord_KeptAsOneChunk()
|
||||
{
|
||||
var longWord = new string('a', 500);
|
||||
var result = IrcMessageFormatter.SplitMessage(longWord, 400);
|
||||
|
||||
// Single word can't be split at word boundary, so it stays as one chunk
|
||||
Assert.Single(result);
|
||||
Assert.Equal(longWord, result[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Server.Auth;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class JwtTokenServiceTests
|
||||
{
|
||||
private const string TestSecret = "this_is_a_test_secret_key_that_is_long_enough_for_hmac_sha256";
|
||||
private const string TestIssuer = "TestIssuer";
|
||||
private const string TestAudience = "TestAudience";
|
||||
|
||||
private static JwtTokenService CreateService(
|
||||
string? secret = null, string? issuer = null, string? audience = null)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Jwt:Secret"] = secret ?? TestSecret,
|
||||
["Jwt:Issuer"] = issuer ?? TestIssuer,
|
||||
["Jwt:Audience"] = audience ?? TestAudience,
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new JwtTokenService(config);
|
||||
}
|
||||
|
||||
private static User CreateUser(
|
||||
string username = "alice",
|
||||
ServerRole role = ServerRole.Member,
|
||||
string? displayName = null) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = username,
|
||||
PasswordHash = "hash",
|
||||
DisplayName = displayName,
|
||||
Role = role,
|
||||
};
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MissingSecret_Throws()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Jwt:Issuer"] = TestIssuer,
|
||||
["Jwt:Audience"] = TestAudience,
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => new JwtTokenService(config));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MissingIssuer_Throws()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Jwt:Secret"] = TestSecret,
|
||||
["Jwt:Audience"] = TestAudience,
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => new JwtTokenService(config));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MissingAudience_Throws()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Jwt:Secret"] = TestSecret,
|
||||
["Jwt:Issuer"] = TestIssuer,
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => new JwtTokenService(config));
|
||||
}
|
||||
|
||||
// ── GenerateAccessToken ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GenerateAccessToken_ContainsExpectedClaims()
|
||||
{
|
||||
var service = CreateService();
|
||||
var user = CreateUser(username: "bob", role: ServerRole.Admin, displayName: "Bob Smith");
|
||||
|
||||
var (token, _) = service.GenerateAccessToken(user);
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
|
||||
Assert.Equal(user.Id.ToString(), jwt.Claims.First(c => c.Type == "sub").Value);
|
||||
Assert.Equal("bob", jwt.Claims.First(c => c.Type == "username").Value);
|
||||
Assert.Equal("Bob Smith", jwt.Claims.First(c => c.Type == "display_name").Value);
|
||||
Assert.Equal("Admin", jwt.Claims.First(c => c.Type == "role").Value);
|
||||
Assert.NotNull(jwt.Claims.FirstOrDefault(c => c.Type == "jti"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAccessToken_ExpiresIn15Minutes()
|
||||
{
|
||||
var service = CreateService();
|
||||
var user = CreateUser();
|
||||
|
||||
var (_, expiresAt) = service.GenerateAccessToken(user);
|
||||
var diff = expiresAt - DateTimeOffset.UtcNow;
|
||||
|
||||
// Should be approximately 15 minutes (allow 30s tolerance)
|
||||
Assert.InRange(diff.TotalMinutes, 14.5, 15.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAccessToken_DifferentTokensForSameUser()
|
||||
{
|
||||
var service = CreateService();
|
||||
var user = CreateUser();
|
||||
|
||||
var (token1, _) = service.GenerateAccessToken(user);
|
||||
var (token2, _) = service.GenerateAccessToken(user);
|
||||
|
||||
Assert.NotEqual(token1, token2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAccessToken_DisplayNameFallsBackToUsername()
|
||||
{
|
||||
var service = CreateService();
|
||||
var user = CreateUser(username: "alice"); // DisplayName is null
|
||||
|
||||
var (token, _) = service.GenerateAccessToken(user);
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
|
||||
Assert.Equal("alice", jwt.Claims.First(c => c.Type == "display_name").Value);
|
||||
}
|
||||
|
||||
// ── GenerateRefreshToken ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GenerateRefreshToken_Returns88CharBase64()
|
||||
{
|
||||
var token = JwtTokenService.GenerateRefreshToken();
|
||||
|
||||
// 64 bytes → 88 base64 characters
|
||||
Assert.Equal(88, token.Length);
|
||||
// Should be valid base64
|
||||
var bytes = Convert.FromBase64String(token);
|
||||
Assert.Equal(64, bytes.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateRefreshToken_UniqueBetweenCalls()
|
||||
{
|
||||
var token1 = JwtTokenService.GenerateRefreshToken();
|
||||
var token2 = JwtTokenService.GenerateRefreshToken();
|
||||
|
||||
Assert.NotEqual(token1, token2);
|
||||
}
|
||||
|
||||
// ── HashToken ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HashToken_DeterministicForSameInput()
|
||||
{
|
||||
var hash1 = JwtTokenService.HashToken("test-token");
|
||||
var hash2 = JwtTokenService.HashToken("test-token");
|
||||
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashToken_DifferentForDifferentInput()
|
||||
{
|
||||
var hash1 = JwtTokenService.HashToken("token-a");
|
||||
var hash2 = JwtTokenService.HashToken("token-b");
|
||||
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashToken_ReturnsBase64String()
|
||||
{
|
||||
var hash = JwtTokenService.HashToken("test-token");
|
||||
|
||||
// SHA256 → 32 bytes → 44 base64 characters
|
||||
var bytes = Convert.FromBase64String(hash);
|
||||
Assert.Equal(32, bytes.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Reflection;
|
||||
using EchoHub.Server.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class LinkEmbedServiceTests
|
||||
{
|
||||
private static readonly MethodInfo ExtractUrlsMethod = typeof(LinkEmbedService)
|
||||
.GetMethod("ExtractUrls", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private static readonly MethodInfo IsPrivateHostMethod = typeof(LinkEmbedService)
|
||||
.GetMethod("IsPrivateHost", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private static readonly MethodInfo ParseOgTagsMethod = typeof(LinkEmbedService)
|
||||
.GetMethod("ParseOgTags", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private static List<string> ExtractUrls(string content) =>
|
||||
(List<string>)ExtractUrlsMethod.Invoke(null, [content])!;
|
||||
|
||||
private static bool IsPrivateHost(Uri uri) =>
|
||||
(bool)IsPrivateHostMethod.Invoke(null, [uri])!;
|
||||
|
||||
private static Dictionary<string, string> ParseOgTags(string html) =>
|
||||
(Dictionary<string, string>)ParseOgTagsMethod.Invoke(null, [html])!;
|
||||
|
||||
// ── ExtractUrls ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_SingleUrl_ReturnsIt()
|
||||
{
|
||||
var urls = ExtractUrls("Check this: https://example.com");
|
||||
Assert.Single(urls);
|
||||
Assert.Equal("https://example.com", urls[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_MultipleUrls_ReturnsAll()
|
||||
{
|
||||
var urls = ExtractUrls("See https://a.com and https://b.com");
|
||||
Assert.Equal(2, urls.Count);
|
||||
Assert.Contains("https://a.com", urls);
|
||||
Assert.Contains("https://b.com", urls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_UrlWithTrailingPunctuation_Trimmed()
|
||||
{
|
||||
var urls = ExtractUrls("Visit https://example.com.");
|
||||
Assert.Single(urls);
|
||||
Assert.Equal("https://example.com", urls[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_NoUrls_ReturnsEmpty()
|
||||
{
|
||||
var urls = ExtractUrls("No links here");
|
||||
Assert.Empty(urls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_MaxUrlsLimit_Respected()
|
||||
{
|
||||
// EmbedMaxUrlsPerMessage = 3
|
||||
var text = "https://a.com https://b.com https://c.com https://d.com https://e.com";
|
||||
var urls = ExtractUrls(text);
|
||||
Assert.Equal(3, urls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_DuplicateUrls_Deduped()
|
||||
{
|
||||
var urls = ExtractUrls("https://example.com and https://example.com again");
|
||||
Assert.Single(urls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractUrls_HttpUrl_Extracted()
|
||||
{
|
||||
var urls = ExtractUrls("http://example.com");
|
||||
Assert.Single(urls);
|
||||
Assert.StartsWith("http://", urls[0]);
|
||||
}
|
||||
|
||||
// ── IsPrivateHost ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_Localhost_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://localhost/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_LoopbackIP_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://127.0.0.1/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_10Network_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://10.0.0.1/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_172_16Network_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://172.16.0.1/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_192_168Network_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://192.168.1.1/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_PublicIP_ReturnsFalse()
|
||||
{
|
||||
Assert.False(IsPrivateHost(new Uri("http://8.8.8.8/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_PublicDomain_ReturnsFalse()
|
||||
{
|
||||
Assert.False(IsPrivateHost(new Uri("https://example.com/test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPrivateHost_ZeroIP_ReturnsTrue()
|
||||
{
|
||||
Assert.True(IsPrivateHost(new Uri("http://0.0.0.0/test")));
|
||||
}
|
||||
|
||||
// ── ParseOgTags ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ParseOgTags_StandardOgTags_ParsedCorrectly()
|
||||
{
|
||||
var html = """
|
||||
<html><head>
|
||||
<meta property="og:title" content="Test Title" />
|
||||
<meta property="og:description" content="A description" />
|
||||
<meta property="og:site_name" content="TestSite" />
|
||||
</head></html>
|
||||
""";
|
||||
var tags = ParseOgTags(html);
|
||||
|
||||
Assert.Equal("Test Title", tags["title"]);
|
||||
Assert.Equal("A description", tags["description"]);
|
||||
Assert.Equal("TestSite", tags["site_name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOgTags_ReversedOrder_ParsedCorrectly()
|
||||
{
|
||||
var html = """<meta content="Reversed Title" property="og:title" />""";
|
||||
var tags = ParseOgTags(html);
|
||||
|
||||
Assert.Equal("Reversed Title", tags["title"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOgTags_NoOgTags_ReturnsEmptyDictionary()
|
||||
{
|
||||
var html = "<html><head><title>Page</title></head></html>";
|
||||
var tags = ParseOgTags(html);
|
||||
|
||||
Assert.Empty(tags);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOgTags_SingleQuotes_ParsedCorrectly()
|
||||
{
|
||||
var html = """<meta property='og:title' content='Single Quoted' />""";
|
||||
var tags = ParseOgTags(html);
|
||||
|
||||
Assert.Equal("Single Quoted", tags["title"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOgTags_DuplicateKeys_FirstWins()
|
||||
{
|
||||
var html = """
|
||||
<meta property="og:title" content="First" />
|
||||
<meta property="og:title" content="Second" />
|
||||
""";
|
||||
var tags = ParseOgTags(html);
|
||||
|
||||
Assert.Equal("First", tags["title"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user