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
This commit is contained in:
Stevan Freeborn
2025-05-22 18:02:41 -05:00
parent 58dc5896ca
commit ceb693acbc
14 changed files with 245 additions and 17 deletions
@@ -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);
}
}
@@ -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)
}
};
}
@@ -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);
}
}
@@ -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<int, int> TestData => new()
{
{ DiscordMessageReferenceTypes.Default, 0 },
{ DiscordMessageReferenceTypes.Forward, 1 },
};
}
@@ -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<ILogger<DiscordRestClient>> _loggerMock = new();
private readonly Mock<ILogger<DiscordRestClient>> _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<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenHttpClientIsNull_ItShouldThrowAnException()
{
var act = () => new DiscordRestClient(_mockLogger.Object, null!);
act.Should().Throw<ArgumentNullException>();
}
[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<DiscordRestClientException>();
}
[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<DiscordRestClientException>();
}
[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();
@@ -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();
}
}
@@ -0,0 +1,31 @@
using StevesBot.Worker.Handlers;
namespace StevesBot.Worker.Tests.Unit;
public class WelcomeMessageHandlerTests
{
private readonly Mock<IDiscordRestClient> _mockDiscordRestClient = new();
private readonly Mock<ILogger<IDiscordGatewayClient>> _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<string>(), It.IsAny<CreateMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Never
);
}
}
+1
View File
@@ -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;
@@ -14,7 +14,7 @@ internal sealed class DiscordRestClient : IDiscordRestClient
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken)
public async Task<string> 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<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken)
public async Task<DiscordMessage> 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);
@@ -2,6 +2,6 @@ namespace StevesBot.Worker.Discord.Rest;
internal interface IDiscordRestClient
{
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken);
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default);
Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken = default);
}
@@ -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
);
@@ -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;
}
@@ -0,0 +1,7 @@
namespace StevesBot.Worker.Discord.Shared;
internal static class DiscordMessageReferenceTypes
{
public const int Default = 0;
public const int Forward = 1;
}
@@ -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<IDiscordRestClient>();
var logger = serviceProvider.GetRequiredService<ILogger<DiscordGatewayClient>>();
var logger = serviceProvider.GetRequiredService<ILogger<IDiscordGatewayClient>>();
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,