summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-07-15 00:26:32 +0200
committerRory& <root@rory.gay>2026-07-15 00:26:32 +0200
commit1829e8d935f0aa6b4e0cb6ceabb4fd633d9cfc1f (patch)
tree5757de134f5510277858269031b28bc8eef32600
parentGHA: pass --accept-flake-config to nix (diff)
downloadserver-ts-1829e8d935f0aa6b4e0cb6ceabb4fd633d9cfc1f.tar.xz
Basic gateway tests
-rw-r--r--.gitignore3
-rw-r--r--extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj2
-rw-r--r--extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs107
-rw-r--r--extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs26
-rw-r--r--extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs39
5 files changed, 167 insertions, 10 deletions
diff --git a/.gitignore b/.gitignore

index 45a5b187..485bcdfa 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 2788c580..47c70400 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 @@ <ItemGroup> <Content Include="appsettings*.json"> - <CopyToOutputDirectory>Never</CopyToOutputDirectory> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> 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 00000000..511b4139 --- /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<TestFixture>(testOutputHelper, fixture), IAsyncLifetime { + private readonly Config _config = fixture.GetRequiredService<Config>(testOutputHelper); + private readonly UserAbstraction _userAbstraction = fixture.GetRequiredService<UserAbstraction>(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<HelloResponse>(); + 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 e87d7f8f..282f5824 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<Message>(); 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 2058e21f..25dabdca 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<JsonObject>())!); return (await resp.Content.ReadFromJsonAsync<Channel>())!; } + + public async Task SendMessageAsync(MessageSchema messageSchema) { + throw new NotImplementedException(); + } } public class SpacebarClientGuild(AuthenticatedSpacebarClient client, long guildId) { @@ -171,6 +176,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGat public ClientWebSocket RawClientWebSocket = new(); public int Sequence; public bool TraceGatewayMessages = false; + private CancellationTokenSource _cts = new CancellationTokenSource(); public IdentifyRequest IdentifyData { get; } = new() { Intents = (GatewayIntentFlags?)0xFFFFFFFF, // too lazy to do math, just gimme everything @@ -182,15 +188,22 @@ public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGat public async Task Connect() { if (RawClientWebSocket.State is WebSocketState.Connecting or WebSocketState.Open) return; + _cts = new CancellationTokenSource(); Sequence = 0; + RawClientWebSocket = new(); await RawClientWebSocket.ConnectAsync(new Uri(wellKnown.Gateway.BaseUrl).AddQuery("encoding", "json"), CancellationToken.None); } + public async Task Disconnect() { + await _cts.CancelAsync(); + await RawClientWebSocket.CloseAsync(closeStatus: WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + } + public async Task Start() { - await foreach (var msg in _runReceiveLoop()) { + await foreach (var msg in _runReceiveLoop(_cts.Token)) { // logger.LogInformation("Got gateway message: {msg}", msg); if (msg.Opcode == GatewayOpcode.S2CHello) { - _ = _runHeartbeatLoop(msg.GetData<HelloResponse>()!.HeartbeatInterval).ContinueWith(ct => { + _ = _runHeartbeatLoop(msg.GetData<HelloResponse>()!.HeartbeatInterval, _cts.Token).ContinueWith(ct => { logger.LogWarning("Heartbeat loop exited!"); if (ct.IsFaulted) throw ct.Exception; }); @@ -226,7 +239,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGat } } - private async Task _runHeartbeatLoop(int interval) { + private async Task _runHeartbeatLoop(int interval, CancellationToken ct) { while (RawClientWebSocket.State < WebSocketState.Closed) { await RawClientWebSocket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new GatewayPayload() { Opcode = GatewayOpcode.C2SQoSHeartbeat, // QoS Heartbeat @@ -238,23 +251,33 @@ public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGat Version = 27 } }.ToJsonNode().AsObject() - }), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); - await Task.Delay(interval); + }), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, ct); + await Task.Delay(interval, ct); } } private const int ReceiveBufferSize = 2 * 1024 * 1024; - private async IAsyncEnumerable<GatewayPayload> _runReceiveLoop() { + private async IAsyncEnumerable<GatewayPayload> _runReceiveLoop([EnumeratorCancellation] CancellationToken ct) { List<byte> 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}");