// Copyright © WireMock.Net using System.Net.WebSockets; using AwesomeAssertions; using WireMock.Matchers; using WireMock.Net.Xunit; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; namespace WireMock.Net.Tests.WebSockets; public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAccessor testContext) { private readonly CancellationToken _ct = testContext.Current.CancellationToken; [Fact] public async Task EchoServer_Should_Echo_Text_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/echo") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithEcho() ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/echo"); // Act await client.ConnectAsync(uri, CancellationToken.None); client.State.Should().Be(WebSocketState.Open); var testMessage = "Hello, WebSocket!"; await client.SendAsync(testMessage, cancellationToken: _ct); // Assert var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(testMessage); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WithText_Should_Send_Configured_Text() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var responseMessage = "This is a predefined response"; server .Given(Request.Create() .WithPath("/ws/message") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .SendMessage(m => m.WithText(responseMessage)) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/message"); // Act await client.ConnectAsync(uri, _ct); client.State.Should().Be(WebSocketState.Open); var testMessage = "Any message from client"; await client.SendAsync(testMessage, cancellationToken: _ct); // Assert var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(responseMessage); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WithText_Should_Send_Same_Text_For_Multiple_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var responseMessage = "Fixed response"; server .Given(Request.Create() .WithPath("/ws/message") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .SendMessage(m => m.WithText(responseMessage)) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/message"); await client.ConnectAsync(uri, _ct); var testMessages = new[] { "First", "Second", "Third" }; // Act & Assert foreach (var testMessage in testMessages) { await client.SendAsync(testMessage, cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(responseMessage, $"should always return the fixed response regardless of input message '{testMessage}'"); } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WithBinary_Should_Send_Configured_Bytes() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var responseBytes = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; server .Given(Request.Create() .WithPath("/ws/binary") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .SendMessage(m => m.WithBinary(responseBytes)) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/binary"); // Act await client.ConnectAsync(uri, _ct); client.State.Should().Be(WebSocketState.Open); var testMessage = "Any message from client"; await client.SendAsync(testMessage, cancellationToken: _ct); // Assert var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct); receivedData.Should().BeEquivalentTo(responseBytes); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WithBinary_Should_Send_Same_Bytes_For_Multiple_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var responseBytes = new byte[] { 0x01, 0x02, 0x03 }; server .Given(Request.Create() .WithPath("/ws/binary") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .SendMessage(m => m.WithBinary(responseBytes)) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/binary"); await client.ConnectAsync(uri, _ct); var testMessages = new[] { "First", "Second", "Third" }; // Act & Assert foreach (var testMessage in testMessages) { await client.SendAsync(testMessage, cancellationToken: _ct); var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct); receivedData.Should().BeEquivalentTo(responseBytes, $"should always return the fixed bytes regardless of input message '{testMessage}'"); } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task EchoServer_Should_Echo_Multiple_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/echo") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithEcho()) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/echo"); await client.ConnectAsync(uri, _ct); var testMessages = new[] { "Hello", "World", "WebSocket", "Test" }; // Act & Assert foreach (var testMessage in testMessages) { await client.SendAsync(testMessage, cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(testMessage, $"message '{testMessage}' should be echoed back"); } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task EchoServer_Should_Echo_Binary_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/echo") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithEcho()) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/echo"); await client.ConnectAsync(uri, _ct); var testData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; // Act await client.SendAsync(new ArraySegment(testData), WebSocketMessageType.Binary, true, _ct); var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct); // Assert receivedData.Should().BeEquivalentTo(testData); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task EchoServer_Should_Handle_Empty_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/echo") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithEcho()) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/echo"); await client.ConnectAsync(uri, _ct); // Act await client.SendAsync(string.Empty, cancellationToken: _ct); var receiveBuffer = new byte[1024]; var result = await client.ReceiveAsync(new ArraySegment(receiveBuffer), _ct); // Assert result.Count.Should().Be(0); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task CustomHandler_Should_Handle_Help_Command() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/chat") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; if (text.StartsWith("/help")) { await context.SendAsync("Available commands: /help, /time, /echo , /upper , /reverse "); } } }) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/chat"); await client.ConnectAsync(uri, _ct); // Act await client.SendAsync("/help", cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); // Assert received.Should().Contain("Available commands"); received.Should().Contain("/help"); received.Should().Contain("/time"); received.Should().Contain("/echo"); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task CustomHandler_Should_Handle_Multiple_Commands_In_Sequence() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/chat") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; if (text.StartsWith("/help")) { await context.SendAsync("Available commands: /help, /time, /echo , /upper , /reverse "); } else if (text.StartsWith("/time")) { await context.SendAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); } else if (text.StartsWith("/echo ")) { await context.SendAsync(text.Substring(6)); } else if (text.StartsWith("/upper ")) { await context.SendAsync(text.Substring(7).ToUpper()); } else if (text.StartsWith("/reverse ")) { var toReverse = text.Substring(9); var reversed = new string(toReverse.Reverse().ToArray()); await context.SendAsync(reversed); } else if (text.StartsWith("/close")) { await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection"); } } }) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/chat"); await client.ConnectAsync(uri, _ct); var commands = new (string, Action)[] { ("/help", response => response.Should().Contain("Available commands")), ("/time", response => response.Should().Contain("Server time")), ("/echo Test", response => response.Should().Be("Test")), ("/upper test", response => response.Should().Be("TEST")), ("/reverse hello", response => response.Should().Be("olleh")) }; // Act & Assert foreach (var (command, assertion) in commands) { await client.SendAsync(command, cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); assertion(received); } await client.SendAsync("/close", cancellationToken: _ct); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WhenMessage_Should_Handle_Multiple_Conditions_Fluently() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/conditional") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(3)) .WhenMessage("/help").ThenSendMessage(m => m.WithText("Available commands")) .WhenMessage("/time").ThenSendMessage(m => m.WithText($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC")) .WhenMessage("/echo *").ThenSendMessage(m => m.WithText("echo response")) .WhenMessage(new ExactMatcher("/exact")).ThenSendMessage(m => m.WithText("is exact")) .WhenMessage(new FuncMatcher(s => s == "/func")).ThenSendMessage(m => m.WithText("is func")) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/conditional"); await client.ConnectAsync(uri, _ct); var testCases = new (string message, string expectedContains)[] { ("/help", "Available commands"), ("/time", "Server time"), ("/echo test", "echo response"), ("/exact", "is exact"), ("/func", "is func") }; // Act & Assert foreach (var (message, expectedContains) in testCases) { await client.SendAsync(message, cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Contain(expectedContains, $"message '{message}' should return response containing '{expectedContains}'"); } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task Request_NoMatch_OnPath_Should_Return404() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/test") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WhenMessage("/test") .ThenSendMessage(m => m.WithText("Test") )) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/abc"); // Act Func connectAction = () => client.ConnectAsync(uri, _ct); // Assert (await connectAction.Should().ThrowAsync()) .WithMessage("The server returned status code '404' when status code '101' was expected."); } [Fact] public async Task Request_NoMatch_OnMessageText_Should_ThrowException() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/test") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(3)) .WhenMessage("/test") .ThenSendMessage(m => m.WithText("Test") )) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/test"); await client.ConnectAsync(uri, _ct); await client.SendAsync("/abc", cancellationToken: _ct); // Act Func receiveAction = () => client.ReceiveAsTextAsync(cancellationToken: _ct); // Assert (await receiveAction.Should().ThrowAsync()) .WithMessage("The remote party closed the WebSocket connection without completing the close handshake."); } [Fact] public async Task WhenMessage_Should_Close_Connection_When_AndClose_Is_Used() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/close") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WhenMessage("/close") .ThenSendMessage(m => m.WithText("Closing connection") .AndClose() )) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/close"); await client.ConnectAsync(uri, _ct); // Act await client.SendAsync("/close", cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); // Assert received.Should().Contain("Closing connection"); // Try to receive again - this will complete the close handshake // and update the client state to Closed try { var receiveBuffer = new byte[1024]; var result = await client.ReceiveAsync(new ArraySegment(receiveBuffer), _ct); // If we get here, the message type should be Close result.MessageType.Should().Be(WebSocketMessageType.Close); } catch (WebSocketException) { // Connection was closed, which is expected } // Verify the connection is CloseReceived client.State.Should().Be(WebSocketState.CloseReceived); } [Fact] public async Task WithTransformer_Should_Transform_Message_Using_Handlebars() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/transform") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .SendMessage(m => m.WithText("{{request.Path}} {{[String.Lowercase] message.Text}}")) ) .WithTransformer() ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/transform"); // Act await client.ConnectAsync(uri, _ct); client.State.Should().Be(WebSocketState.Open); var testMessage = "HellO"; await client.SendAsync(testMessage, cancellationToken: _ct); // Assert var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be("/ws/transform hello"); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); } [Fact] public async Task WithWebSocketProxy_Should_Proxy_Multiple_TextMessages() { // Arrange - Start target echo server using var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); exampleEchoServer .Given(Request.Create() .WithPath("/ws/target") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithEcho()) ); // Arrange - Start proxy server using var sut = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); sut .Given(Request.Create() .WithPath("/ws/proxy") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocketProxy($"{exampleEchoServer.Url}/ws/target") ); using var client = new ClientWebSocket(); var proxyUri = new Uri($"{sut.Url}/ws/proxy"); await client.ConnectAsync(proxyUri, _ct); var testMessages = new[] { "First", "Second", "Third" }; // Act & Assert foreach (var testMessage in testMessages) { await client.SendAsync(testMessage, cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(testMessage, $"message '{testMessage}' should be proxied and echoed back"); } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(100, _ct); } [Fact] public async Task WithWebSocketProxy_Should_Proxy_Binary_Messages() { // Arrange - Start target echo server using var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); exampleEchoServer .Given(Request.Create() .WithPath("/ws/target") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws.WithEcho()) ); // Arrange - Start proxy server using var sut = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); sut .Given(Request.Create() .WithPath("/ws/proxy") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocketProxy($"{exampleEchoServer.Url}/ws/target") ); using var client = new ClientWebSocket(); var proxyUri = new Uri($"{sut.Url}/ws/proxy"); await client.ConnectAsync(proxyUri, _ct); var testData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; // Act await client.SendAsync(new ArraySegment(testData), WebSocketMessageType.Binary, true, _ct); var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct); // Assert receivedData.Should().BeEquivalentTo(testData, "binary data should be proxied and echoed back"); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(100, _ct); } [Fact] public async Task Broadcast_Should_Send_TextMessage_To_Multiple_Connected_Clients() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var broadcastMessage = "Broadcast to all clients"; server .Given(Request.Create() .WithPath("/ws/broadcast") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithBroadcast() .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; if (text == "register") { await context.SendAsync($"Registered: {context.ConnectionId}"); } else if (text.StartsWith("broadcast:")) { var broadcastText = text.Substring(10); await context.BroadcastAsync(broadcastText); } } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); using var client3 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast"); // Act await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); await client3.ConnectAsync(uri, _ct); await client1.SendAsync("register", cancellationToken: _ct); await client2.SendAsync("register", cancellationToken: _ct); await client3.SendAsync("register", cancellationToken: _ct); // Receive registration confirmations var reg1 = await client1.ReceiveAsTextAsync(cancellationToken: _ct); var reg2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); var reg3 = await client3.ReceiveAsTextAsync(cancellationToken: _ct); reg1.Should().StartWith("Registered: "); reg2.Should().StartWith("Registered: "); reg3.Should().StartWith("Registered: "); // Send broadcast from client1 await client1.SendAsync($"broadcast:{broadcastMessage}", cancellationToken: _ct); // Assert - all clients should receive the broadcast var received1 = await client1.ReceiveAsTextAsync(cancellationToken: _ct); var received2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); var received3 = await client3.ReceiveAsTextAsync(cancellationToken: _ct); received1.Should().Be(broadcastMessage); received2.Should().Be(broadcastMessage); received3.Should().Be(broadcastMessage); await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client3.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(300, _ct); } [Fact] public async Task Broadcast_Should_Send_BinaryMessage_To_Multiple_Connected_Clients() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var message = new byte[] { 0x00, 0x01, 0x02, 0x03 }; var broadcastMessageFromWireMock = new byte[] { 0x01, 0x02, 0x03, 0x04 }; server .Given(Request.Create() .WithPath("/ws/broadcast") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithBroadcast() .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text && message.Text == "register") { await context.SendAsync($"Registered: {context.ConnectionId}"); } if (message.MessageType == WebSocketMessageType.Binary) { await context.BroadcastAsync(broadcastMessageFromWireMock); } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); using var client3 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast"); // Act await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); await client3.ConnectAsync(uri, _ct); await client1.SendAsync("register", cancellationToken: _ct); await client2.SendAsync("register", cancellationToken: _ct); await client3.SendAsync("register", cancellationToken: _ct); // Receive registration confirmations var reg1 = await client1.ReceiveAsTextAsync(cancellationToken: _ct); var reg2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); var reg3 = await client3.ReceiveAsTextAsync(cancellationToken: _ct); reg1.Should().StartWith("Registered: "); reg2.Should().StartWith("Registered: "); reg3.Should().StartWith("Registered: "); // Send broadcast from client1 await client1.SendAsync(new ArraySegment(message), WebSocketMessageType.Binary, true, cancellationToken: _ct); // Assert - all clients should receive the broadcast var received1 = await client1.ReceiveAsBytesAsync(cancellationToken: _ct); var received2 = await client2.ReceiveAsBytesAsync(cancellationToken: _ct); var received3 = await client3.ReceiveAsBytesAsync(cancellationToken: _ct); received1.Should().BeEquivalentTo(broadcastMessageFromWireMock); received2.Should().BeEquivalentTo(broadcastMessageFromWireMock); received3.Should().BeEquivalentTo(broadcastMessageFromWireMock); await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client3.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(300, _ct); } [Fact] public async Task Broadcast_Should_Handle_Multiple_Broadcast_Messages() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/broadcast-multi") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithBroadcast() .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; await context.BroadcastAsync(text); } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast-multi"); await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); var messages = new[] { "Message 1", "Message 2", "Message 3" }; // Act & Assert foreach (var message in messages) { await client1.SendAsync(message, cancellationToken: _ct); var received1 = await client1.ReceiveAsTextAsync(cancellationToken: _ct); var received2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); received1.Should().Be(message); received2.Should().Be(message); } await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(300, _ct); } [Fact] public async Task Broadcast_Should_Exclude_Sender_When_ExcludeSender_Is_True() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/broadcast-exclude") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithBroadcast() .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; if (text.StartsWith("send:")) { var broadcastText = text.Substring(5); await context.BroadcastAsync(broadcastText, excludeSender: true); } } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast-exclude"); await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); var broadcastMessage = "Exclusive broadcast"; // Act await client1.SendAsync($"send:{broadcastMessage}", cancellationToken: _ct); // Assert - only client2 should receive the message var received2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); received2.Should().Be(broadcastMessage); // client1 should not receive anything (or should timeout) var receiveTask1 = client1.ReceiveAsTextAsync(cancellationToken: _ct); var delayTask = Task.Delay(500, _ct); var completedTask = await Task.WhenAny(receiveTask1, delayTask); completedTask.Should().Be(delayTask, "client1 should not receive the exclusive broadcast"); await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(200, _ct); } [Fact] public async Task Broadcast_Should_Work_With_Single_Client() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var broadcastMessage = "Single client broadcast"; server .Given(Request.Create() .WithPath("/ws/broadcast-single") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithBroadcast() .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; await context.BroadcastAsync(text); } }) ) ); using var client = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast-single"); // Act await client.ConnectAsync(uri, _ct); await client.SendAsync(broadcastMessage, cancellationToken: _ct); // Assert var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(broadcastMessage); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(100, _ct); } [Fact] public async Task Broadcast_Should_Handle_Client_Disconnect_During_Broadcast() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); var broadcastMessage = "Message after disconnect"; server .Given(Request.Create() .WithPath("/ws/broadcast-disconnect") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithBroadcast() .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; await context.BroadcastAsync(text); } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast-disconnect"); await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); // Act - disconnect client1 await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", _ct); // Send broadcast from client2 - should handle disconnected client gracefully await client2.SendAsync(broadcastMessage, cancellationToken: _ct); // Assert - client2 should still receive the broadcast var received2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); received2.Should().Be(broadcastMessage); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(200, _ct); } [Fact] public async Task Broadcast_Should_Support_Targeted_Broadcasting_Based_On_Condition() { // Arrange using var server = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] }); server .Given(Request.Create() .WithPath("/ws/broadcast-conditional") .WithWebSocketUpgrade() ) .RespondWith(Response.Create() .WithWebSocket(ws => ws .WithCloseTimeout(TimeSpan.FromSeconds(10)) .WithBroadcast() .WithMessageHandler(async (message, context) => { if (message.MessageType == WebSocketMessageType.Text) { var text = message.Text ?? string.Empty; if (text.StartsWith("to-admins:")) { var adminMessage = text.Substring(10); await context.SendAsync($"Admin broadcast: {adminMessage}"); } else if (text.StartsWith("to-all:")) { var allMessage = text.Substring(7); await context.BroadcastAsync(allMessage); } } }) ) ); using var client1 = new ClientWebSocket(); using var client2 = new ClientWebSocket(); var uri = new Uri($"{server.Url}/ws/broadcast-conditional"); await client1.ConnectAsync(uri, _ct); await client2.ConnectAsync(uri, _ct); // Act await client1.SendAsync("to-all:General message", cancellationToken: _ct); // Assert - both clients receive the broadcast var received1 = await client1.ReceiveAsTextAsync(cancellationToken: _ct); var received2 = await client2.ReceiveAsTextAsync(cancellationToken: _ct); received1.Should().Be("General message"); received2.Should().Be("General message"); await client1.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client2.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await Task.Delay(200, _ct); } }