diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs index 6ea423e..2eec841 100644 --- a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs @@ -18,6 +18,18 @@ public sealed class DiscordGatewayClientTests : IDisposable .Setup(static x => x.GetUtcNow()) .Returns(DateTimeOffset.UtcNow); + var mockServiceProvider = new Mock(); + + var mockServiceScope = new Mock(); + + mockServiceScope + .Setup(static x => x.ServiceProvider) + .Returns(mockServiceProvider.Object); + + _mockServiceScopeFactory + .Setup(static x => x.CreateScope()) + .Returns(mockServiceScope.Object); + _discordGatewayClient = new DiscordGatewayClient( _options, _mockWebSocketFactory.Object, @@ -1056,9 +1068,92 @@ public sealed class DiscordGatewayClientTests : IDisposable } [Fact] - public Task On_WhenCalledWithValidEventAndHandler_ItShouldSubscribeHandlerToEvent() + public async Task On_WhenCalledWithValidEventAndHandler_ItShouldSubscribeHandlerToEvent() { - throw new NotImplementedException(); + var readyHandler = new Mock>(); + var firstCreateMessageHandler = new Mock>(); + var secondCreateMessageHandler = new Mock>(); + + secondCreateMessageHandler + .Setup(static x => x(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("This is a test exception")); + + _discordGatewayClient.On(DiscordEventTypes.Ready, readyHandler.Object); + _discordGatewayClient.On(DiscordEventTypes.MessageCreate, firstCreateMessageHandler.Object); + _discordGatewayClient.On(DiscordEventTypes.MessageCreate, secondCreateMessageHandler.Object); + + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var socketState = WebSocketState.Closed; + + var mockWebSocket = new Mock(); + + mockWebSocket + .Setup(static x => x.State) + .Returns(() => socketState); + + mockWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => socketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var messageCreateEvent = new MessageCreateDiscordEvent(); + var messageCreatePayload = CreateEventPayload(messageCreateEvent); + var messageCreateResult = new WebSocketReceiveResult( + messageCreatePayload.Bytes.Length, + WebSocketMessageType.Text, + true + ); + messagesToReceive.Enqueue((messageCreateResult, messageCreatePayload.Bytes)); + + SetupReceiveMessageSequence(mockWebSocket, messagesToReceive); + + _mockWebSocketFactory + .Setup(static x => x.Create()) + .Returns(mockWebSocket.Object); + + using var cts = new CancellationTokenSource(); + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + readyHandler.Invocations.Should().HaveCount(1); + firstCreateMessageHandler.Invocations.Should().HaveCount(1); + secondCreateMessageHandler.Invocations.Should().HaveCount(1); } [Fact] @@ -1077,9 +1172,92 @@ public sealed class DiscordGatewayClientTests : IDisposable } [Fact] - public Task Off_WhenCalledWithValidEventAndHandler_ItShouldUnsubscribeHandlerFromEvent() + public async Task Off_WhenCalledWithValidEventAndHandler_ItShouldUnsubscribeHandlerFromEvent() { - throw new NotImplementedException(); + var readyHandler = new Mock>(); + var firstCreateMessageHandler = new Mock>(); + var secondCreateMessageHandler = new Mock>(); + + _discordGatewayClient.On(DiscordEventTypes.Ready, readyHandler.Object); + _discordGatewayClient.On(DiscordEventTypes.MessageCreate, firstCreateMessageHandler.Object); + _discordGatewayClient.On(DiscordEventTypes.MessageCreate, secondCreateMessageHandler.Object); + + _discordGatewayClient.Off(DiscordEventTypes.MessageCreate, firstCreateMessageHandler.Object); + _discordGatewayClient.Off(DiscordEventTypes.Ready, readyHandler.Object); + _discordGatewayClient.Off(DiscordEventTypes.Ready, readyHandler.Object); + + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var socketState = WebSocketState.Closed; + + var mockWebSocket = new Mock(); + + mockWebSocket + .Setup(static x => x.State) + .Returns(() => socketState); + + mockWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => socketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var messageCreateEvent = new MessageCreateDiscordEvent(); + var messageCreatePayload = CreateEventPayload(messageCreateEvent); + var messageCreateResult = new WebSocketReceiveResult( + messageCreatePayload.Bytes.Length, + WebSocketMessageType.Text, + true + ); + messagesToReceive.Enqueue((messageCreateResult, messageCreatePayload.Bytes)); + + SetupReceiveMessageSequence(mockWebSocket, messagesToReceive); + + _mockWebSocketFactory + .Setup(static x => x.Create()) + .Returns(mockWebSocket.Object); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + readyHandler.Invocations.Should().HaveCount(0); + firstCreateMessageHandler.Invocations.Should().HaveCount(0); } private static void SetupReceiveMessageSequence( diff --git a/src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs b/src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs index cb1f005..536e951 100644 --- a/src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs @@ -8,7 +8,7 @@ public class MessageCreateDiscordEventTests var result = new MessageCreateDiscordEvent(); result.OpCode.Should().Be(0); - result.Type.Should().BeNull(); + result.Type.Should().Be(DiscordEventTypes.MessageCreate); result.Sequence.Should().BeNull(); result.Data.Should().BeEquivalentTo(new DiscordMessage()); } diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs index 6bb2bf8..23e6645 100644 --- a/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs @@ -5,6 +5,11 @@ internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent [JsonPropertyName("d")] public new DiscordMessage Data { get; init; } = new DiscordMessage(); + public MessageCreateDiscordEvent() + { + Type = DiscordEventTypes.MessageCreate; + } + public bool IsMessageType(int messageType) { return Data.Type == messageType; diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs index 0bcaa2a..afbdcbb 100644 --- a/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs @@ -2,9 +2,13 @@ namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record ReadyDiscordEvent : DispatchDiscordEvent { - [JsonPropertyName("t")] - public new string Type { get; init; } = DiscordEventTypes.Ready; [JsonPropertyName("d")] public new ReadyData Data { get; init; } = new ReadyData(); + + public ReadyDiscordEvent() + { + Type = DiscordEventTypes.Ready; + } } +