From ceb693acbce8160fab38092abfad199738b00cfb Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 22 May 2025 18:02:41 -0500 Subject: [PATCH] tests: add unit tests for CreateMessageRequest, DiscordMessageReference, and DiscordRestClient refactor: update DiscordRestClient and IDiscordRestClient to use default CancellationToken refactor: rename MessageReference to DiscordMessageReference and add DiscordMessageReferenceTypes chore: update WelcomeMessageHandler to use new DiscordMessageReference --- .../Unit/CreateMessageRequestTests.cs | 22 ++++++ .../Unit/DiscordEventConverterTests.cs | 40 ++++++++++ .../Unit/DiscordMessageReferenceTests.cs | 28 +++++++ .../Unit/DiscordMessageReferenceTypesTests.cs | 17 ++++ .../Unit/DiscordRestClientTests.cs | 77 ++++++++++++++++++- .../Unit/LockReleaserTests.cs | 15 ++++ .../Unit/WelcomeMessageHandlerTests.cs | 31 ++++++++ src/StevesBot.Worker.Tests/Usings.cs | 1 + .../Discord/Rest/DiscordRestClient.cs | 4 +- .../Discord/Rest/IDiscordRestClient.cs | 4 +- .../Rest/Requests/CreateMessageRequest.cs | 2 +- .../Discord/Shared/DiscordMessageReference.cs | 8 +- .../Shared/DiscordMessageReferenceTypes.cs | 7 ++ .../Handlers/WelcomeMessageHandler.cs | 6 +- 14 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs create mode 100644 src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs create mode 100644 src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs create mode 100644 src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs create mode 100644 src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs diff --git a/src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs b/src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs new file mode 100644 index 0000000..597ee76 --- /dev/null +++ b/src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs @@ -0,0 +1,22 @@ +namespace StevesBot.Worker.Tests.Unit; + +public class CreateMessageRequestTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var content = "content"; + var messageReference = new DiscordMessageReference( + 1, + "message_id", + "channel_id", + "guild_id", + false + ); + + var result = new CreateMessageRequest(content, messageReference); + + result.Content.Should().Be(content); + result.MessageReference.Should().BeSameAs(messageReference); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs index e05361a..33c0b18 100644 --- a/src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs @@ -81,6 +81,16 @@ public class DiscordEventConverterTests }, typeof(ReadyDiscordEvent) }, + { + new + { + op = DiscordOpCodes.Dispatch, + s = null as int?, + t = DiscordEventTypes.MessageCreate, + d = null as object + }, + typeof(MessageCreateDiscordEvent) + }, { new { @@ -100,6 +110,36 @@ public class DiscordEventConverterTests d = null as object }, typeof(DiscordEvent) + }, + { + new + { + op = 1, + s = null as int?, + t = null as string, + d = null as object + }, + typeof(HeartbeatDiscordEvent) + }, + { + new + { + op = 7, + s = null as int?, + t = null as string, + d = null as object + }, + typeof(ReconnectDiscordEvent) + }, + { + new + { + op = 9, + s = null as int?, + t = null as string, + d = false + }, + typeof(InvalidSessionDiscordEvent) } }; } \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs new file mode 100644 index 0000000..d96d002 --- /dev/null +++ b/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs @@ -0,0 +1,28 @@ +namespace StevesBot.Worker.Tests.Unit; + +public class DiscordMessageReferenceTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var type = 1; + var messageId = "message_id"; + var channelId = "channel_id"; + var guildId = "guild_id"; + var failIfNotExists = true; + + var result = new DiscordMessageReference( + type, + messageId, + channelId, + guildId, + failIfNotExists + ); + + result.Type.Should().Be(type); + result.MessageId.Should().Be(messageId); + result.ChannelId.Should().Be(channelId); + result.GuildId.Should().Be(guildId); + result.FailIfNotExists.Should().Be(failIfNotExists); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs new file mode 100644 index 0000000..cc00aca --- /dev/null +++ b/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs @@ -0,0 +1,17 @@ +namespace StevesBot.Worker.Tests.Unit; + +public class DiscordMessageReferenceTypesTests +{ + [Theory] + [MemberData(nameof(TestData))] + public void Type_WhenCalled_ItShouldReturnExpectedValue(int type, int expected) + { + type.Should().Be(expected); + } + + public static TheoryData TestData => new() + { + { DiscordMessageReferenceTypes.Default, 0 }, + { DiscordMessageReferenceTypes.Forward, 1 }, + }; +} \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs index 6458516..17749d4 100644 --- a/src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs @@ -1,11 +1,14 @@ +using Microsoft.AspNetCore.Http.HttpResults; + namespace StevesBot.Worker.Tests.Unit; public sealed class DiscordRestClientTests : IDisposable { private const string BaseUrl = "https://discord.com/api/v10"; private static string GatewayEndpoint => $"{BaseUrl}/gateway"; + private static string ChannelMessagesEndpoint => $"{BaseUrl}/channels/*/messages"; - private readonly Mock> _loggerMock = new(); + private readonly Mock> _mockLogger = new(); private readonly MockHttpMessageHandler _mockHttpMessageHandler; private readonly DiscordRestClient _discordRestClient; @@ -14,7 +17,23 @@ public sealed class DiscordRestClientTests : IDisposable _mockHttpMessageHandler = new MockHttpMessageHandler(); var httpClient = _mockHttpMessageHandler.ToHttpClient(); httpClient.BaseAddress = new Uri("https://discord.com/api/v10/"); - _discordRestClient = new DiscordRestClient(_loggerMock.Object, httpClient); + _discordRestClient = new DiscordRestClient(_mockLogger.Object, httpClient); + } + + [Fact] + public void Constructor_WhenLoggerIsNull_ItShouldThrowAnException() + { + var act = () => new DiscordRestClient(null!, _mockHttpMessageHandler.ToHttpClient()); + + act.Should().Throw(); + } + + [Fact] + public void Constructor_WhenHttpClientIsNull_ItShouldThrowAnException() + { + var act = () => new DiscordRestClient(_mockLogger.Object, null!); + + act.Should().Throw(); } [Fact] @@ -58,6 +77,60 @@ public sealed class DiscordRestClientTests : IDisposable result.Should().Be(expectedUrl); } + [Fact] + public async Task CreateMessageAsync_WhenRequestFails_ItShouldThrowAnException() + { + _mockHttpMessageHandler + .When(ChannelMessagesEndpoint) + .Respond(HttpStatusCode.InternalServerError); + + var request = new CreateMessageRequest( + "content", + new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false) + ); + + var act = async () => await _discordRestClient.CreateMessageAsync("channel_id", request); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task CreateMessageyAsync_WhenResponseIsNull_ItShouldThrowAnException() + { + _mockHttpMessageHandler + .When(ChannelMessagesEndpoint) + .Respond(HttpStatusCode.OK, "application/json", "null"); + + var request = new CreateMessageRequest( + "content", + new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false) + ); + + var act = async () => await _discordRestClient.CreateMessageAsync("channel_id", request); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task CreateMessageAsync_WhenRequestSucceeds_ItShouldReturnMessage() + { + var message = new DiscordMessage(); + var messageResponse = JsonSerializer.Serialize(message); + + _mockHttpMessageHandler + .When(ChannelMessagesEndpoint) + .Respond(HttpStatusCode.OK, "application/json", messageResponse); + + var request = new CreateMessageRequest( + "content", + new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false) + ); + + var result = await _discordRestClient.CreateMessageAsync("channel_id", request); + + result.Should().BeEquivalentTo(message); + } + public void Dispose() { _mockHttpMessageHandler.Dispose(); diff --git a/src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs b/src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs index 5b534e8..72d71b3 100644 --- a/src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs @@ -42,4 +42,19 @@ public class LockReleaserTests semaphore.CurrentCount.Should().Be(1); } + + [Fact] + public void Dispose_WhenCalledAndSemaphoreIsAlreadyDisposed_ItShouldNotThrowException() + { + var act = () => + { + var semaphore = new SemaphoreSlim(0, 1); + var lockReleaser = new LockReleaser(semaphore); + + semaphore.Dispose(); + lockReleaser.Dispose(); + }; + + act.Should().NotThrow(); + } } \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs b/src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs new file mode 100644 index 0000000..45a72bc --- /dev/null +++ b/src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs @@ -0,0 +1,31 @@ +using StevesBot.Worker.Handlers; + +namespace StevesBot.Worker.Tests.Unit; + +public class WelcomeMessageHandlerTests +{ + private readonly Mock _mockDiscordRestClient = new(); + private readonly Mock> _mockLogger = new(); + private readonly IServiceProvider _serviceProvider; + + public WelcomeMessageHandlerTests() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(_mockDiscordRestClient.Object); + serviceCollection.AddSingleton(_mockLogger.Object); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + [Fact] + public async Task HandleAsync_WhenEventIsNotMessageCreateEvent_ItShouldNotCreateMessage() + { + await WelcomeMessageHandler.HandleAsync(new DiscordEvent(), _serviceProvider); + + _mockDiscordRestClient + .Verify( + static c => c.CreateMessageAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never + ); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker.Tests/Usings.cs b/src/StevesBot.Worker.Tests/Usings.cs index b5db2a1..3e07822 100644 --- a/src/StevesBot.Worker.Tests/Usings.cs +++ b/src/StevesBot.Worker.Tests/Usings.cs @@ -21,6 +21,7 @@ global using StevesBot.Worker.Discord.Gateway; global using StevesBot.Worker.Discord.Gateway.Events; global using StevesBot.Worker.Discord.Gateway.Events.Data; global using StevesBot.Worker.Discord.Rest; +global using StevesBot.Worker.Discord.Rest.Requests; global using StevesBot.Worker.Discord.Shared; global using StevesBot.Worker.Tests.Integration.Infrastructure; global using StevesBot.Worker.Threading; diff --git a/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs b/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs index c3c8383..2a11459 100644 --- a/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs +++ b/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs @@ -14,7 +14,7 @@ internal sealed class DiscordRestClient : IDiscordRestClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } - public async Task GetGatewayUrlAsync(CancellationToken cancellationToken) + public async Task GetGatewayUrlAsync(CancellationToken cancellationToken = default) { var gatewayEndpoint = new Uri("gateway", UriKind.Relative); var response = await _httpClient.GetAsync(gatewayEndpoint, cancellationToken); @@ -36,7 +36,7 @@ internal sealed class DiscordRestClient : IDiscordRestClient return gatewayResponse.Url; } - public async Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken) + public async Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken = default) { var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative); var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken); diff --git a/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs b/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs index e9a8e8d..a1f1f0b 100644 --- a/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs +++ b/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs @@ -2,6 +2,6 @@ namespace StevesBot.Worker.Discord.Rest; internal interface IDiscordRestClient { - Task GetGatewayUrlAsync(CancellationToken cancellationToken); - Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken); + Task GetGatewayUrlAsync(CancellationToken cancellationToken = default); + Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs b/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs index 8203c18..cf96e12 100644 --- a/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs +++ b/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs @@ -2,5 +2,5 @@ namespace StevesBot.Worker.Discord.Rest.Requests; internal sealed record CreateMessageRequest( [property: JsonPropertyName("content")] string Content, - [property: JsonPropertyName("message_reference")] MessageReference? MessageReference + [property: JsonPropertyName("message_reference")] DiscordMessageReference? MessageReference ); \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs index f911493..78cc184 100644 --- a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs +++ b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs @@ -1,15 +1,9 @@ namespace StevesBot.Worker.Discord.Shared; -internal sealed record MessageReference( +internal sealed record DiscordMessageReference( [property: JsonPropertyName("type")] int Type, [property: JsonPropertyName("message_id")] string MessageId, [property: JsonPropertyName("channel_id")] string ChannelId, [property: JsonPropertyName("guild_id")] string GuildId, [property: JsonPropertyName("fail_if_not_exists")] bool FailIfNotExists ); - -internal static class MessageReferenceTypes -{ - public const int Default = 0; - public const int Forward = 1; -} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs new file mode 100644 index 0000000..8d43052 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Worker.Discord.Shared; + +internal static class DiscordMessageReferenceTypes +{ + public const int Default = 0; + public const int Forward = 1; +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs b/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs index 7f895dd..ff95a4c 100644 --- a/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs +++ b/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs @@ -6,11 +6,11 @@ internal static class WelcomeMessageHandler public static async Task HandleAsync( DiscordEvent discordEvent, IServiceProvider serviceProvider, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ) { var discordRestClient = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); + var logger = serviceProvider.GetRequiredService>(); if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false) { @@ -23,7 +23,7 @@ internal static class WelcomeMessageHandler var request = new CreateMessageRequest( Content: welcomeMessage, MessageReference: new( - Type: MessageReferenceTypes.Default, + Type: DiscordMessageReferenceTypes.Default, MessageId: mcde.Data.Id, ChannelId: mcde.Data.ChannelId, GuildId: mcde.Data.GuildId,