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,