diff --git a/.gitignore b/.gitignore index 45a5b1870..485bcdfa3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ assets/cache config.json assets/cacheMisses http-client.private.env.json +.nixos-test-history .vscode/settings.json @@ -34,4 +35,4 @@ schemas_final/ temp/ !/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/ -*.qcow2 +*.qcow2 \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj index 2788c5801..47c704006 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj +++ b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj @@ -45,7 +45,7 @@ - Never + Always diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs new file mode 100644 index 000000000..511b41391 --- /dev/null +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using Spacebar.Models.Gateway; +using Spacebar.Models.Generic; +using Spacebar.Sdk.Core; +using Spacebar.Tests.Abstractions; +using Spacebar.Tests.Extensions; +using Spacebar.Tests.Fixtures; +using Xunit.Internal; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace Spacebar.Tests.Tests; + +public class GatewayTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture), IAsyncLifetime { + private readonly Config _config = fixture.GetRequiredService(testOutputHelper); + private readonly UserAbstraction _userAbstraction = fixture.GetRequiredService(testOutputHelper); + + private static AuthenticatedSpacebarClient Client { get; set; } = null!; + + public async ValueTask InitializeAsync() { + testOutputHelper.WriteLine("Running InitializeAsync"); + // All these tests can share a single client + Client = await _userAbstraction.GetSharedUser(); + } + + [Fact] + public async Task CanConnect() { + await Client.Gateway.Connect(); + await Client.Gateway.Disconnect(); + } + + [Fact] + public async Task CanReceiveReady() { + Client.Gateway.OnceGatewayMessage.Add(async payload => { + if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) { + _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + await Client.Gateway.Disconnect(); + return true; + } + + _testOutputHelper.WriteLine("Received message: {0}", payload.ToJson(indent: false)); + return false; + }); + // Client.Gateway.TraceGatewayMessages = true; + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } + + [Fact] + public async Task CanReceiveReadySupplemental() { + Client.Gateway.OnceGatewayMessage.Add(async payload => { + if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY_SUPPLEMENTAL" }) { + _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + await Client.Gateway.Disconnect(); + return true; + } + + _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count); + return false; + }); + // Client.Gateway.TraceGatewayMessages = true; + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } + + + [Fact] + public async Task CanReceiveHeartbeatAck() { + Client.Gateway.OnceGatewayMessage.Add(async payload => { + if (payload is { Opcode: GatewayOpcode.S2CHeartbeatAck }) { + _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + await Client.Gateway.Disconnect(); + return true; + } + + _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count); + return false; + }); + // Client.Gateway.TraceGatewayMessages = true; + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } + + [Fact] + public async Task SensibleHello() { + Client.Gateway.OnceGatewayMessage.Add(async payload => { + if (payload is { Opcode: GatewayOpcode.S2CHello }) { + _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + var data = payload.GetData(); + await Client.Gateway.Disconnect(); + + Assert.NotEqual(0, data.HeartbeatInterval); + Assert.True(data.HeartbeatInterval > 1000, "data.HeartbeatInterval > 1000"); + return true; + } + + _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count); + return false; + }); + // Client.Gateway.TraceGatewayMessages = true; + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs index e87d7f8fc..282f5824b 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using ArcaneLibs.Extensions; using Spacebar.Models.Api; +using Spacebar.Models.Gateway; using Spacebar.Models.Generic; using Spacebar.Sdk.Core; using Spacebar.Tests.Abstractions; @@ -222,4 +223,29 @@ public class MessageTests(ITestOutputHelper testOutputHelper, TestFixture fixtur var msg = json.Deserialize(); Assert.Equal(content, msg.Content); } + + [Fact] + public async Task ShouldNotMessageUpdateForLinklessMessage() { + Client.Gateway.OnceGatewayMessage.Add(async payload => { + if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) { + _testOutputHelper.WriteLine("Got ready: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + await Channel.SendMessageAsync(new MessageSchema() { + + }); + return false; + } + + if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "MESSAGE_UPDATE" }) { + _testOutputHelper.WriteLine("Got MESSAGE_UPDATE: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count); + Assert.False(true); + return true; + } + + _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count); + return false; + }); + // Client.Gateway.TraceGatewayMessages = true; + await Client.Gateway.Connect(); + await Client.Gateway.Start(); + } } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs index 2058e21f3..25dabdca1 100644 --- a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs +++ b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Net.Http.Json; using System.Net.WebSockets; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -128,6 +129,10 @@ public class SpacebarClientChannel(AuthenticatedSpacebarClient client, long chan if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync())!); return (await resp.Content.ReadFromJsonAsync())!; } + + public async Task SendMessageAsync(MessageSchema messageSchema) { + throw new NotImplementedException(); + } } public class SpacebarClientGuild(AuthenticatedSpacebarClient client, long guildId) { @@ -171,6 +176,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger()!.HeartbeatInterval).ContinueWith(ct => { + _ = _runHeartbeatLoop(msg.GetData()!.HeartbeatInterval, _cts.Token).ContinueWith(ct => { logger.LogWarning("Heartbeat loop exited!"); if (ct.IsFaulted) throw ct.Exception; }); @@ -226,7 +239,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger _runReceiveLoop() { + private async IAsyncEnumerable _runReceiveLoop([EnumeratorCancellation] CancellationToken ct) { List messageParts = []; List<(string Name, TimeSpan Elapsed)> trace = []; var buffer = new byte[ReceiveBufferSize]; int idx = 0; - while (RawClientWebSocket.State < WebSocketState.Closed) { + while (RawClientWebSocket.State < WebSocketState.CloseSent) { var sw = Stopwatch.StartNew(); - var msg = await RawClientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + WebSocketReceiveResult msg; + try { + msg = await RawClientWebSocket.ReceiveAsync(buffer, ct); + } + catch (TaskCanceledException) { + yield break; + } + catch (InvalidOperationException) { + yield break; + } + trace.Add(($"RCV.{idx}", sw.GetElapsedAndRestart())); Console.WriteLine($"Websocket message chunk read: {msg.MessageType} {msg.Count} {msg.EndOfMessage}");