mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-22 16:28:27 +02:00
Add WebSockets
This commit is contained in:
@@ -154,6 +154,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.NET8.W
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebApplication.IIS", "examples\WireMock.Net.WebApplication.IIS\WireMock.Net.WebApplication.IIS.csproj", "{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebApplication.IIS", "examples\WireMock.Net.WebApplication.IIS\WireMock.Net.WebApplication.IIS.csproj", "{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebSocketExamples", "examples\WireMock.Net.WebSocketExamples\WireMock.Net.WebSocketExamples.csproj", "{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -788,6 +790,18 @@ Global
|
|||||||
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x64.Build.0 = Release|Any CPU
|
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.ActiveCfg = Release|Any CPU
|
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.Build.0 = Release|Any CPU
|
{9957038D-F9C3-CA5D-E8AE-BE188E512635}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{2D86546D-8A24-0A55-C962-2071BD299E05}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -812,18 +826,18 @@ Global
|
|||||||
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x64.Build.0 = Release|Any CPU
|
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.ActiveCfg = Release|Any CPU
|
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.Build.0 = Release|Any CPU
|
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x64.Build.0 = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|x86.Build.0 = Debug|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.ActiveCfg = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x64.Build.0 = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.ActiveCfg = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|x86.Build.0 = Release|Any CPU
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -883,9 +897,10 @@ Global
|
|||||||
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
||||||
{C8F4E6D2-9A3B-4F1C-8D5E-7A2B3C4D5E6F} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
{C8F4E6D2-9A3B-4F1C-8D5E-7A2B3C4D5E6F} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
||||||
{9957038D-F9C3-CA5D-E8AE-BE188E512635} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
{9957038D-F9C3-CA5D-E8AE-BE188E512635} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
||||||
|
{4005E20C-D42B-138A-79BE-B3F5420C563F} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
||||||
{2D86546D-8A24-0A55-C962-2071BD299E05} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
{2D86546D-8A24-0A55-C962-2071BD299E05} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
||||||
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
{5E6E9FA7-9135-7B82-2CCD-8CA87AC8043C} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
||||||
{4005E20C-D42B-138A-79BE-B3F5420C563F} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
|
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
|
||||||
|
|||||||
725
examples/WireMock.Net.WebSocketExamples/Program.cs
Normal file
725
examples/WireMock.Net.WebSocketExamples/Program.cs
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using WireMock.Logging;
|
||||||
|
using WireMock.RequestBuilders;
|
||||||
|
using WireMock.ResponseBuilders;
|
||||||
|
using WireMock.Server;
|
||||||
|
using WireMock.Settings;
|
||||||
|
|
||||||
|
namespace WireMock.Net.WebSocketExamples;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("WireMock.Net WebSocket Examples");
|
||||||
|
Console.WriteLine("================================\n");
|
||||||
|
|
||||||
|
Console.WriteLine("Choose an example to run:");
|
||||||
|
Console.WriteLine("1. Echo Server");
|
||||||
|
Console.WriteLine("2. Custom Message Handler");
|
||||||
|
Console.WriteLine("3. Broadcast Server");
|
||||||
|
Console.WriteLine("4. Scenario/State Machine");
|
||||||
|
Console.WriteLine("5. WebSocket Proxy");
|
||||||
|
Console.WriteLine("6. Multiple WebSocket Endpoints");
|
||||||
|
Console.WriteLine("7. All Examples (runs all endpoints)");
|
||||||
|
Console.WriteLine("0. Exit\n");
|
||||||
|
|
||||||
|
Console.Write("Enter choice: ");
|
||||||
|
var choice = Console.ReadLine();
|
||||||
|
|
||||||
|
switch (choice)
|
||||||
|
{
|
||||||
|
case "1":
|
||||||
|
await RunEchoServerExample();
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
await RunCustomMessageHandlerExample();
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
await RunBroadcastExample();
|
||||||
|
break;
|
||||||
|
case "4":
|
||||||
|
await RunScenarioExample();
|
||||||
|
break;
|
||||||
|
case "5":
|
||||||
|
await RunProxyExample();
|
||||||
|
break;
|
||||||
|
case "6":
|
||||||
|
await RunMultipleEndpointsExample();
|
||||||
|
break;
|
||||||
|
case "7":
|
||||||
|
await RunAllExamples();
|
||||||
|
break;
|
||||||
|
case "0":
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
Console.WriteLine("Invalid choice");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 1: Simple Echo Server
|
||||||
|
/// Echoes back all messages received from the client
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunEchoServerExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== Echo Server Example ===");
|
||||||
|
Console.WriteLine("Starting WebSocket echo server...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/echo")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithEcho()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Echo server listening at: {server.Urls[0]}/ws/echo");
|
||||||
|
Console.WriteLine("\nTest with a WebSocket client:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/echo");
|
||||||
|
Console.WriteLine("\nPress any key to test or CTRL+C to exit...");
|
||||||
|
Console.ReadKey();
|
||||||
|
|
||||||
|
// Test the echo server
|
||||||
|
await TestWebSocketEcho(server.Urls[0]);
|
||||||
|
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 2: Custom Message Handler
|
||||||
|
/// Processes messages and sends custom responses
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunCustomMessageHandlerExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== Custom Message Handler Example ===");
|
||||||
|
Console.WriteLine("Starting WebSocket server with custom message handler...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Handle different commands
|
||||||
|
if (text.StartsWith("/help"))
|
||||||
|
{
|
||||||
|
await context.SendTextAsync("Available commands: /help, /time, /echo <text>, /upper <text>, /reverse <text>");
|
||||||
|
}
|
||||||
|
else if (text.StartsWith("/time"))
|
||||||
|
{
|
||||||
|
await context.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||||
|
}
|
||||||
|
else if (text.StartsWith("/echo "))
|
||||||
|
{
|
||||||
|
await context.SendTextAsync(text.Substring(6));
|
||||||
|
}
|
||||||
|
else if (text.StartsWith("/upper "))
|
||||||
|
{
|
||||||
|
await context.SendTextAsync(text.Substring(7).ToUpper());
|
||||||
|
}
|
||||||
|
else if (text.StartsWith("/reverse "))
|
||||||
|
{
|
||||||
|
var toReverse = text.Substring(9);
|
||||||
|
var reversed = new string(toReverse.Reverse().ToArray());
|
||||||
|
await context.SendTextAsync(reversed);
|
||||||
|
}
|
||||||
|
else if (text == "/quit")
|
||||||
|
{
|
||||||
|
await context.SendTextAsync("Goodbye!");
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client requested disconnect");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.SendTextAsync($"Unknown command: {text}. Type /help for available commands.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Chat server listening at: {server.Urls[0]}/ws/chat");
|
||||||
|
Console.WriteLine("\nTest with:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/chat");
|
||||||
|
Console.WriteLine("\nThen try commands: /help, /time, /echo hello, /upper hello, /reverse hello");
|
||||||
|
Console.WriteLine("\nPress any key to test or CTRL+C to exit...");
|
||||||
|
Console.ReadKey();
|
||||||
|
|
||||||
|
await TestWebSocketChat(server.Urls[0]);
|
||||||
|
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 3: Broadcast Server
|
||||||
|
/// Broadcasts messages to all connected clients
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunBroadcastExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== Broadcast Server Example ===");
|
||||||
|
Console.WriteLine("Starting WebSocket broadcast server...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
var broadcastMappingGuid = Guid.NewGuid();
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/broadcast")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.WithGuid(broadcastMappingGuid)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithBroadcast()
|
||||||
|
.WithMessageHandler(async (message, context) =>
|
||||||
|
{
|
||||||
|
if (message.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
var text = message.Text ?? string.Empty;
|
||||||
|
var timestamp = DateTime.UtcNow.ToString("HH:mm:ss");
|
||||||
|
var broadcastMessage = $"[{timestamp}] Broadcast: {text}";
|
||||||
|
|
||||||
|
// Broadcast to all connected clients
|
||||||
|
await context.BroadcastTextAsync(broadcastMessage);
|
||||||
|
|
||||||
|
Console.WriteLine($"Broadcasted to {server.GetWebSocketConnections(broadcastMappingGuid).Count} clients: {text}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Broadcast server listening at: {server.Urls[0]}/ws/broadcast");
|
||||||
|
Console.WriteLine("\nConnect multiple clients:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/broadcast");
|
||||||
|
Console.WriteLine("\nMessages sent from any client will be broadcast to all clients");
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 4: Scenario/State Machine
|
||||||
|
/// Demonstrates state transitions during WebSocket session
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunScenarioExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== Scenario/State Machine Example ===");
|
||||||
|
Console.WriteLine("Starting WebSocket server with scenario support...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state: Waiting for players
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/game")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.InScenario("GameSession")
|
||||||
|
.WillSetStateTo("Lobby")
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("Welcome to the game lobby! Type 'ready' to start or 'quit' to leave.");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lobby state: Waiting for ready
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/game")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.InScenario("GameSession")
|
||||||
|
.WhenStateIs("Lobby")
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
var text = msg.Text?.ToLower() ?? string.Empty;
|
||||||
|
|
||||||
|
if (text == "ready")
|
||||||
|
{
|
||||||
|
ctx.SetScenarioState("Playing");
|
||||||
|
await ctx.SendTextAsync("Game started! Type 'attack' to attack, 'defend' to defend, or 'quit' to exit.");
|
||||||
|
}
|
||||||
|
else if (text == "quit")
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("You left the lobby. Goodbye!");
|
||||||
|
await ctx.CloseAsync(WebSocketCloseStatus.NormalClosure, "Player quit");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("In lobby. Type 'ready' to start or 'quit' to leave.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Playing state: Game is active
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/game")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.InScenario("GameSession")
|
||||||
|
.WhenStateIs("Playing")
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
var text = msg.Text?.ToLower() ?? string.Empty;
|
||||||
|
|
||||||
|
if (text == "attack")
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("You attacked! Critical hit! 💥");
|
||||||
|
}
|
||||||
|
else if (text == "defend")
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("You defended! Shield up! 🛡️");
|
||||||
|
}
|
||||||
|
else if (text == "quit")
|
||||||
|
{
|
||||||
|
ctx.SetScenarioState("GameOver");
|
||||||
|
await ctx.SendTextAsync("Game over! Thanks for playing.");
|
||||||
|
await ctx.CloseAsync(WebSocketCloseStatus.NormalClosure, "Game ended");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("Unknown action. Type 'attack', 'defend', or 'quit'.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Game server listening at: {server.Urls[0]}/ws/game");
|
||||||
|
Console.WriteLine("\nConnect and follow the game flow:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/game");
|
||||||
|
Console.WriteLine("\nGame flow: Lobby -> Type 'ready' -> Playing -> Type 'attack'/'defend' -> Type 'quit'");
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 5: WebSocket Proxy
|
||||||
|
/// Proxies WebSocket connections to another server
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunProxyExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== WebSocket Proxy Example ===");
|
||||||
|
Console.WriteLine("Starting WebSocket proxy server...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/proxy")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocketProxy("ws://echo.websocket.org")
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Proxy server listening at: {server.Urls[0]}/ws/proxy");
|
||||||
|
Console.WriteLine("Proxying to: ws://echo.websocket.org");
|
||||||
|
Console.WriteLine("\nTest with:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/proxy");
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 6: Multiple WebSocket Endpoints
|
||||||
|
/// Demonstrates running multiple WebSocket endpoints simultaneously
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunMultipleEndpointsExample()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== Multiple WebSocket Endpoints Example ===");
|
||||||
|
Console.WriteLine("Starting server with multiple WebSocket endpoints...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger(),
|
||||||
|
WebSocketSettings = new WebSocketSettings
|
||||||
|
{
|
||||||
|
MaxConnections = 100,
|
||||||
|
KeepAliveIntervalSeconds = 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint 1: Echo
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/echo")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws.WithEcho())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Endpoint 2: Time service
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/time")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Endpoint 3: JSON service
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/json")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
message = msg.Text,
|
||||||
|
length = msg.Text?.Length ?? 0,
|
||||||
|
type = msg.MessageType.ToString()
|
||||||
|
};
|
||||||
|
await ctx.SendJsonAsync(response);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Endpoint 4: Protocol-specific
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/protocol")
|
||||||
|
.WithWebSocketUpgrade("chat", "superchat")
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithAcceptProtocol("chat")
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync($"Using protocol: chat. Message: {msg.Text}");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine("Available WebSocket endpoints:");
|
||||||
|
Console.WriteLine($" 1. Echo: {server.Urls[0]}/ws/echo");
|
||||||
|
Console.WriteLine($" 2. Time: {server.Urls[0]}/ws/time");
|
||||||
|
Console.WriteLine($" 3. JSON: {server.Urls[0]}/ws/json");
|
||||||
|
Console.WriteLine($" 4. Protocol: {server.Urls[0]}/ws/protocol");
|
||||||
|
Console.WriteLine("\nTest with wscat:");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/echo");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/time");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/json");
|
||||||
|
Console.WriteLine(" wscat -c ws://localhost:9091/ws/protocol -s chat");
|
||||||
|
Console.WriteLine("\nPress any key to stop server...");
|
||||||
|
Console.ReadKey();
|
||||||
|
server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example 7: Run All Examples
|
||||||
|
/// Starts a server with all example endpoints
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunAllExamples()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n=== All Examples Running ===");
|
||||||
|
Console.WriteLine("Starting server with all WebSocket endpoints...\n");
|
||||||
|
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
Logger = new WireMockConsoleLogger(),
|
||||||
|
WebSocketSettings = new WebSocketSettings
|
||||||
|
{
|
||||||
|
MaxConnections = 200
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SetupAllEndpoints(server);
|
||||||
|
|
||||||
|
Console.WriteLine("All WebSocket endpoints are running:");
|
||||||
|
Console.WriteLine($" Echo: {server.Urls[0]}/ws/echo");
|
||||||
|
Console.WriteLine($" Chat: {server.Urls[0]}/ws/chat");
|
||||||
|
Console.WriteLine($" Broadcast: {server.Urls[0]}/ws/broadcast");
|
||||||
|
Console.WriteLine($" Game: {server.Urls[0]}/ws/game");
|
||||||
|
Console.WriteLine($" Time: {server.Urls[0]}/ws/time");
|
||||||
|
Console.WriteLine($" JSON: {server.Urls[0]}/ws/json");
|
||||||
|
Console.WriteLine("\nServer statistics:");
|
||||||
|
Console.WriteLine($" Total mappings: {server.Mappings.Count}");
|
||||||
|
|
||||||
|
Console.WriteLine("\nPress any key to view connection stats or CTRL+C to exit...");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Console.ReadKey(true);
|
||||||
|
var connections = server.GetWebSocketConnections();
|
||||||
|
Console.WriteLine($"\nActive WebSocket connections: {connections.Count}");
|
||||||
|
foreach (var conn in connections)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {conn.ConnectionId}: {conn.RequestMessage.Path} (State: {conn.WebSocket.State})");
|
||||||
|
}
|
||||||
|
Console.WriteLine("\nPress any key to refresh or CTRL+C to exit...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupAllEndpoints(WireMockServer server)
|
||||||
|
{
|
||||||
|
// Echo endpoint
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/echo")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws.WithEcho())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chat endpoint
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/chat")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (message, context) =>
|
||||||
|
{
|
||||||
|
if (message.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
await context.SendTextAsync($"Echo: {message.Text}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Broadcast endpoint
|
||||||
|
var broadcastGuid = Guid.NewGuid();
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/broadcast")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.WithGuid(broadcastGuid)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithBroadcast()
|
||||||
|
.WithMessageHandler(async (message, context) =>
|
||||||
|
{
|
||||||
|
if (message.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
await context.BroadcastTextAsync($"[Broadcast] {message.Text}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Game scenario endpoint
|
||||||
|
SetupGameScenario(server);
|
||||||
|
|
||||||
|
// Time endpoint
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/time")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync($"Server time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// JSON endpoint
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/json")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
message = msg.Text,
|
||||||
|
connectionId = ctx.ConnectionId
|
||||||
|
};
|
||||||
|
await ctx.SendJsonAsync(response);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupGameScenario(WireMockServer server)
|
||||||
|
{
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/game")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.InScenario("GameSession")
|
||||||
|
.WillSetStateTo("Lobby")
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.SendTextAsync("Welcome! Type 'ready' to start.");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create()
|
||||||
|
.WithPath("/ws/game")
|
||||||
|
.WithWebSocketUpgrade()
|
||||||
|
)
|
||||||
|
.InScenario("GameSession")
|
||||||
|
.WhenStateIs("Lobby")
|
||||||
|
.RespondWith(Response.Create()
|
||||||
|
.WithWebSocket(ws => ws
|
||||||
|
.WithMessageHandler(async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
if (msg.Text?.ToLower() == "ready")
|
||||||
|
{
|
||||||
|
ctx.SetScenarioState("Playing");
|
||||||
|
await ctx.SendTextAsync("Game started!");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for testing
|
||||||
|
private static async Task TestWebSocketEcho(string baseUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new ClientWebSocket();
|
||||||
|
var uri = new Uri($"{baseUrl.Replace("http://", "ws://")}/ws/echo");
|
||||||
|
|
||||||
|
Console.WriteLine($"\nConnecting to {uri}...");
|
||||||
|
await client.ConnectAsync(uri, CancellationToken.None);
|
||||||
|
Console.WriteLine("Connected!");
|
||||||
|
|
||||||
|
var testMessages = new[] { "Hello", "World", "WebSocket", "Test" };
|
||||||
|
|
||||||
|
foreach (var testMessage in testMessages)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nSending: {testMessage}");
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(testMessage);
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
var received = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
Console.WriteLine($"Received: {received}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
|
||||||
|
Console.WriteLine("\nTest completed successfully!");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nTest failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TestWebSocketChat(string baseUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new ClientWebSocket();
|
||||||
|
var uri = new Uri($"{baseUrl.Replace("http://", "ws://")}/ws/chat");
|
||||||
|
|
||||||
|
Console.WriteLine($"\nConnecting to {uri}...");
|
||||||
|
await client.ConnectAsync(uri, CancellationToken.None);
|
||||||
|
Console.WriteLine("Connected!");
|
||||||
|
|
||||||
|
var commands = new[] { "/help", "/time", "/echo Hello", "/upper test", "/reverse hello" };
|
||||||
|
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nSending: {command}");
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(command);
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
var received = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
Console.WriteLine($"Received: {received}");
|
||||||
|
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
|
||||||
|
Console.WriteLine("\nTest completed successfully!");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nTest failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
examples/WireMock.Net.WebSocketExamples/README.md
Normal file
156
examples/WireMock.Net.WebSocketExamples/README.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# WireMock.Net WebSocket Examples
|
||||||
|
|
||||||
|
This project demonstrates all the WebSocket capabilities of WireMock.Net.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- .NET 8.0 SDK
|
||||||
|
- Optional: `wscat` for manual testing (`npm install -g wscat`)
|
||||||
|
|
||||||
|
## Running the Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/WireMock.Net.WebSocketExamples
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Examples
|
||||||
|
|
||||||
|
### 1. Echo Server
|
||||||
|
Simple WebSocket echo server that returns all messages back to the client.
|
||||||
|
|
||||||
|
**Test with:**
|
||||||
|
```bash
|
||||||
|
wscat -c ws://localhost:9091/ws/echo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Custom Message Handler
|
||||||
|
Chat server with commands: `/help`, `/time`, `/echo`, `/upper`, `/reverse`, `/quit`
|
||||||
|
|
||||||
|
**Test with:**
|
||||||
|
```bash
|
||||||
|
wscat -c ws://localhost:9091/ws/chat
|
||||||
|
> /help
|
||||||
|
> /time
|
||||||
|
> /echo Hello World
|
||||||
|
> /upper test
|
||||||
|
> /reverse hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Broadcast Server
|
||||||
|
Messages sent by any client are broadcast to all connected clients.
|
||||||
|
|
||||||
|
**Test with multiple terminals:**
|
||||||
|
```bash
|
||||||
|
# Terminal 1
|
||||||
|
wscat -c ws://localhost:9091/ws/broadcast
|
||||||
|
|
||||||
|
# Terminal 2
|
||||||
|
wscat -c ws://localhost:9091/ws/broadcast
|
||||||
|
|
||||||
|
# Terminal 3
|
||||||
|
wscat -c ws://localhost:9091/ws/broadcast
|
||||||
|
```
|
||||||
|
|
||||||
|
Type messages in any terminal and see them appear in all terminals.
|
||||||
|
|
||||||
|
### 4. Scenario/State Machine
|
||||||
|
Game server with state transitions: Lobby -> Playing -> GameOver
|
||||||
|
|
||||||
|
**Test with:**
|
||||||
|
```bash
|
||||||
|
wscat -c ws://localhost:9091/ws/game
|
||||||
|
> ready
|
||||||
|
> attack
|
||||||
|
> defend
|
||||||
|
> quit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. WebSocket Proxy
|
||||||
|
Proxies WebSocket connections to echo.websocket.org
|
||||||
|
|
||||||
|
**Test with:**
|
||||||
|
```bash
|
||||||
|
wscat -c ws://localhost:9091/ws/proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Multiple Endpoints
|
||||||
|
Runs multiple WebSocket endpoints simultaneously:
|
||||||
|
- `/ws/echo` - Echo server
|
||||||
|
- `/ws/time` - Returns server time
|
||||||
|
- `/ws/json` - Returns JSON responses
|
||||||
|
- `/ws/protocol` - Protocol-specific endpoint
|
||||||
|
|
||||||
|
### 7. All Examples
|
||||||
|
Runs all endpoints at once with connection statistics.
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
- ✅ **Echo Server** - Simple message echo
|
||||||
|
- ✅ **Custom Handlers** - Complex message processing
|
||||||
|
- ✅ **Broadcast** - Multi-client communication
|
||||||
|
- ✅ **Scenarios** - State machine patterns
|
||||||
|
- ✅ **Proxy** - Forwarding to real WebSocket servers
|
||||||
|
- ✅ **Protocol Negotiation** - Sec-WebSocket-Protocol support
|
||||||
|
- ✅ **JSON Messaging** - Structured data exchange
|
||||||
|
- ✅ **Connection Management** - Track and manage connections
|
||||||
|
- ✅ **Configuration** - Custom WebSocket settings
|
||||||
|
|
||||||
|
## Testing with wscat
|
||||||
|
|
||||||
|
Install wscat globally:
|
||||||
|
```bash
|
||||||
|
npm install -g wscat
|
||||||
|
```
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
```bash
|
||||||
|
# Connect to endpoint
|
||||||
|
wscat -c ws://localhost:9091/ws/echo
|
||||||
|
|
||||||
|
# Connect with protocol
|
||||||
|
wscat -c ws://localhost:9091/ws/protocol -s chat
|
||||||
|
|
||||||
|
# Connect with headers
|
||||||
|
wscat -c ws://localhost:9091/ws/echo -H "X-Custom-Header: value"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with C# Client
|
||||||
|
|
||||||
|
The examples include built-in C# WebSocket clients for automated testing.
|
||||||
|
Select options 1 or 2 and press any key to run the automated tests.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
WebSocket settings can be configured:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
Port = 9091,
|
||||||
|
WebSocketSettings = new WebSocketSettings
|
||||||
|
{
|
||||||
|
MaxConnections = 100,
|
||||||
|
ReceiveBufferSize = 8192,
|
||||||
|
MaxMessageSize = 1048576,
|
||||||
|
KeepAliveInterval = TimeSpan.FromSeconds(30),
|
||||||
|
CloseTimeout = TimeSpan.FromMinutes(10),
|
||||||
|
EnableCompression = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
When running "All Examples" (option 7), press any key to view:
|
||||||
|
- Active connection count
|
||||||
|
- Connection IDs
|
||||||
|
- Request paths
|
||||||
|
- WebSocket states
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All examples run on port 9091 by default
|
||||||
|
- Press CTRL+C to stop the server
|
||||||
|
- Multiple clients can connect simultaneously
|
||||||
|
- Connection states are tracked and can be queried
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<AssemblyName>WireMock.Net.WebSocketExamples</AssemblyName>
|
||||||
|
<RootNamespace>WireMock.Net.WebSocketExamples</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -131,4 +131,9 @@ public class SettingsModel
|
|||||||
/// Whether to accept any client certificate
|
/// Whether to accept any client certificate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AcceptAnyClientCertificate { get; set; }
|
public bool AcceptAnyClientCertificate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the WebSocket settings.
|
||||||
|
/// </summary>
|
||||||
|
public WebSocketSettingsModel? WebSocketSettings { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.Admin.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket Settings Model
|
||||||
|
/// </summary>
|
||||||
|
[FluentBuilder.AutoGenerateBuilder]
|
||||||
|
public class WebSocketSettingsModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of concurrent WebSocket connections (default: 100)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxConnections { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default receive buffer size in bytes (default: 4096)
|
||||||
|
/// </summary>
|
||||||
|
public int ReceiveBufferSize { get; set; } = 4096;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default keep-alive interval in seconds (default: 30)
|
||||||
|
/// </summary>
|
||||||
|
public int KeepAliveIntervalSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum message size in bytes (default: 1048576 - 1 MB)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxMessageSize { get; set; } = 1048576;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable WebSocket compression (default: true)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableCompression { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default close timeout in minutes (default: 10)
|
||||||
|
/// </summary>
|
||||||
|
public int CloseTimeoutMinutes { get; set; } = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket constants
|
||||||
|
/// </summary>
|
||||||
|
public static class WebSocketConstants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default receive buffer size for WebSocket messages (4 KB)
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultReceiveBufferSize = 4096;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default keep-alive interval in seconds
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultKeepAliveIntervalSeconds = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default close timeout in minutes
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultCloseTimeoutMinutes = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum buffer size for WebSocket operations (1 KB)
|
||||||
|
/// </summary>
|
||||||
|
public const int MinimumBufferSize = 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default maximum message size (1 MB)
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultMaxMessageSize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy forward buffer size (4 KB)
|
||||||
|
/// </summary>
|
||||||
|
public const int ProxyForwardBufferSize = 4096;
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||||
<!-- CVE-2018-8292 / https://github.com/advisories/GHSA-7jgj-8wvc-jh57 -->
|
<!-- CVE-2018-8292 / https://github.com/advisories/GHSA-7jgj-8wvc-jh57 -->
|
||||||
<PackageReference Include="System.Net.Http " Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ internal partial class AspNetCoreSelfHost : IOwinSelfHost
|
|||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
UseCors(appBuilder);
|
UseCors(appBuilder);
|
||||||
|
|
||||||
|
var webSocketOptions = new WebSocketOptions();
|
||||||
|
if (_wireMockMiddlewareOptions.WebSocketSettings?.KeepAliveIntervalSeconds != null)
|
||||||
|
{
|
||||||
|
webSocketOptions.KeepAliveInterval = TimeSpan.FromSeconds(_wireMockMiddlewareOptions.WebSocketSettings.KeepAliveIntervalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
appBuilder.UseWebSockets(webSocketOptions);
|
||||||
#endif
|
#endif
|
||||||
_wireMockMiddlewareOptions.PreWireMockMiddlewareInit?.Invoke(appBuilder);
|
_wireMockMiddlewareOptions.PreWireMockMiddlewareInit?.Invoke(appBuilder);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using WireMock.Matchers;
|
|||||||
using WireMock.Settings;
|
using WireMock.Settings;
|
||||||
using WireMock.Types;
|
using WireMock.Types;
|
||||||
using WireMock.Util;
|
using WireMock.Util;
|
||||||
|
using WireMock.WebSockets;
|
||||||
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
|
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
|
||||||
|
|
||||||
namespace WireMock.Owin;
|
namespace WireMock.Owin;
|
||||||
@@ -82,11 +83,21 @@ internal interface IWireMockMiddlewareOptions
|
|||||||
|
|
||||||
QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; }
|
QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; }
|
||||||
|
|
||||||
public bool ProxyAll { get; set; }
|
bool ProxyAll { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the activity tracing options.
|
/// Gets or sets the activity tracing options.
|
||||||
/// When set, System.Diagnostics.Activity objects are created for request tracing.
|
/// When set, System.Diagnostics.Activity objects are created for request tracing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The WebSocket connection registries per mapping (used for broadcast).
|
||||||
|
/// </summary>
|
||||||
|
ConcurrentDictionary<Guid, WebSocketConnectionRegistry> WebSocketRegistries { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket settings.
|
||||||
|
/// </summary>
|
||||||
|
WebSocketSettings? WebSocketSettings { get; set; }
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ using RandomDataGenerator.Randomizers;
|
|||||||
using Stef.Validation;
|
using Stef.Validation;
|
||||||
using WireMock.Http;
|
using WireMock.Http;
|
||||||
using WireMock.ResponseBuilders;
|
using WireMock.ResponseBuilders;
|
||||||
|
using WireMock.ResponseProviders;
|
||||||
using WireMock.Types;
|
using WireMock.Types;
|
||||||
using WireMock.Util;
|
using WireMock.Util;
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ namespace WireMock.Owin.Mappers
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
|
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
|
||||||
{
|
{
|
||||||
if (responseMessage == null)
|
if (responseMessage == null || responseMessage is WebSocketHandledResponse)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ namespace WireMock.Owin;
|
|||||||
internal class WireMockMiddleware
|
internal class WireMockMiddleware
|
||||||
{
|
{
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private static readonly Task CompletedTask = Task.FromResult(false);
|
|
||||||
|
|
||||||
private readonly IWireMockMiddlewareOptions _options;
|
private readonly IWireMockMiddlewareOptions _options;
|
||||||
private readonly IOwinRequestMapper _requestMapper;
|
private readonly IOwinRequestMapper _requestMapper;
|
||||||
@@ -66,6 +65,9 @@ internal class WireMockMiddleware
|
|||||||
|
|
||||||
private async Task InvokeInternalAsync(HttpContext ctx)
|
private async Task InvokeInternalAsync(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
// Store options in HttpContext for providers to access (e.g., WebSocketResponseProvider)
|
||||||
|
ctx.Items[nameof(WireMockMiddlewareOptions)] = _options;
|
||||||
|
|
||||||
var request = await _requestMapper.MapAsync(ctx.Request, _options).ConfigureAwait(false);
|
var request = await _requestMapper.MapAsync(ctx.Request, _options).ConfigureAwait(false);
|
||||||
|
|
||||||
var logRequest = false;
|
var logRequest = false;
|
||||||
@@ -144,9 +146,7 @@ internal class WireMockMiddleware
|
|||||||
var (theResponse, theOptionalNewMapping) = await targetMapping.ProvideResponseAsync(ctx, request).ConfigureAwait(false);
|
var (theResponse, theOptionalNewMapping) = await targetMapping.ProvideResponseAsync(ctx, request).ConfigureAwait(false);
|
||||||
response = theResponse;
|
response = theResponse;
|
||||||
|
|
||||||
var responseBuilder = targetMapping.Provider as Response;
|
if (targetMapping.Provider is Response responseBuilder && !targetMapping.IsAdminInterface && theOptionalNewMapping != null)
|
||||||
|
|
||||||
if (!targetMapping.IsAdminInterface && theOptionalNewMapping != null)
|
|
||||||
{
|
{
|
||||||
if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping.Settings.ProxyAndRecordSettings?.SaveMapping == true)
|
if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping.Settings.ProxyAndRecordSettings?.SaveMapping == true)
|
||||||
{
|
{
|
||||||
@@ -227,8 +227,6 @@ internal class WireMockMiddleware
|
|||||||
await _responseMapper.MapAsync(notFoundResponse, ctx.Response).ConfigureAwait(false);
|
await _responseMapper.MapAsync(notFoundResponse, ctx.Response).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await CompletedTask.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendToWebhooksAsync(IMapping mapping, IRequestMessage request, IResponseMessage response)
|
private async Task SendToWebhooksAsync(IMapping mapping, IRequestMessage request, IResponseMessage response)
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using WireMock.Handlers;
|
using WireMock.Handlers;
|
||||||
using WireMock.Logging;
|
using WireMock.Logging;
|
||||||
using WireMock.Matchers;
|
using WireMock.Matchers;
|
||||||
using WireMock.Owin.ActivityTracing;
|
|
||||||
using WireMock.Settings;
|
using WireMock.Settings;
|
||||||
using WireMock.Types;
|
using WireMock.Types;
|
||||||
using WireMock.Util;
|
using WireMock.Util;
|
||||||
|
using WireMock.WebSockets;
|
||||||
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
|
using ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode;
|
||||||
|
|
||||||
namespace WireMock.Owin;
|
namespace WireMock.Owin;
|
||||||
@@ -40,7 +40,6 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
|||||||
|
|
||||||
public Action<IApplicationBuilder>? PostWireMockMiddlewareInit { get; set; }
|
public Action<IApplicationBuilder>? PostWireMockMiddlewareInit { get; set; }
|
||||||
|
|
||||||
//#if USE_ASPNETCORE
|
|
||||||
public Action<IServiceCollection>? AdditionalServiceRegistration { get; set; }
|
public Action<IServiceCollection>? AdditionalServiceRegistration { get; set; }
|
||||||
|
|
||||||
public CorsPolicyOptions? CorsPolicyOptions { get; set; }
|
public CorsPolicyOptions? CorsPolicyOptions { get; set; }
|
||||||
@@ -49,7 +48,6 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool AcceptAnyClientCertificate { get; set; }
|
public bool AcceptAnyClientCertificate { get; set; }
|
||||||
//#endif
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IWireMockMiddlewareOptions.FileSystemHandler"/>
|
/// <inheritdoc cref="IWireMockMiddlewareOptions.FileSystemHandler"/>
|
||||||
public IFileSystemHandler? FileSystemHandler { get; set; }
|
public IFileSystemHandler? FileSystemHandler { get; set; }
|
||||||
@@ -107,4 +105,9 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ConcurrentDictionary<Guid, WebSocketConnectionRegistry> WebSocketRegistries { get; } = new();
|
||||||
|
|
||||||
|
public WebSocketSettings? WebSocketSettings { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using WireMock.Matchers;
|
||||||
|
using WireMock.Matchers.Request;
|
||||||
|
|
||||||
|
namespace WireMock.RequestBuilders;
|
||||||
|
|
||||||
|
public partial class Request
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRequestBuilder WithWebSocketUpgrade(params string[] protocols)
|
||||||
|
{
|
||||||
|
_requestMatchers.Add(new RequestMessageHeaderMatcher(
|
||||||
|
MatchBehaviour.AcceptOnMatch,
|
||||||
|
MatchOperator.Or,
|
||||||
|
"Upgrade",
|
||||||
|
true,
|
||||||
|
new ExactMatcher(true, "websocket")
|
||||||
|
));
|
||||||
|
|
||||||
|
_requestMatchers.Add(new RequestMessageHeaderMatcher(
|
||||||
|
MatchBehaviour.AcceptOnMatch,
|
||||||
|
MatchOperator.Or,
|
||||||
|
"Connection",
|
||||||
|
true,
|
||||||
|
new WildcardMatcher("*Upgrade*", true)
|
||||||
|
));
|
||||||
|
|
||||||
|
if (protocols.Length > 0)
|
||||||
|
{
|
||||||
|
_requestMatchers.Add(new RequestMessageHeaderMatcher(
|
||||||
|
MatchBehaviour.AcceptOnMatch,
|
||||||
|
MatchOperator.Or,
|
||||||
|
"Sec-WebSocket-Protocol",
|
||||||
|
true,
|
||||||
|
protocols.Select(p => new ExactMatcher(true, p)).ToArray()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using WireMock.Settings;
|
||||||
|
using WireMock.WebSockets;
|
||||||
|
|
||||||
|
namespace WireMock.ResponseBuilders;
|
||||||
|
|
||||||
|
public partial class Response
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal property to store WebSocket builder configuration
|
||||||
|
/// </summary>
|
||||||
|
internal WebSocketBuilder? WebSocketBuilder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configure WebSocket response behavior
|
||||||
|
/// </summary>
|
||||||
|
public IResponseBuilder WithWebSocket(Action<IWebSocketBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new WebSocketBuilder();
|
||||||
|
configure(builder);
|
||||||
|
|
||||||
|
WebSocketBuilder = builder;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy WebSocket to another server
|
||||||
|
/// </summary>
|
||||||
|
public IResponseBuilder WithWebSocketProxy(string targetUrl)
|
||||||
|
{
|
||||||
|
return WithWebSocketProxy(new ProxyAndRecordSettings { Url = targetUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy WebSocket to another server with settings
|
||||||
|
/// </summary>
|
||||||
|
public IResponseBuilder WithWebSocketProxy(ProxyAndRecordSettings settings)
|
||||||
|
{
|
||||||
|
var builder = new WebSocketBuilder();
|
||||||
|
builder.WithProxy(settings);
|
||||||
|
|
||||||
|
WebSocketBuilder = builder;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using WireMock.ResponseBuilders;
|
|||||||
using WireMock.Types;
|
using WireMock.Types;
|
||||||
using WireMock.Util;
|
using WireMock.Util;
|
||||||
using Stef.Validation;
|
using Stef.Validation;
|
||||||
|
using WireMock.WebSockets;
|
||||||
|
|
||||||
namespace WireMock;
|
namespace WireMock;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Stef.Validation;
|
||||||
|
using WireMock.Constants;
|
||||||
|
using WireMock.Owin;
|
||||||
|
using WireMock.Settings;
|
||||||
|
using WireMock.WebSockets;
|
||||||
|
|
||||||
|
namespace WireMock.ResponseProviders;
|
||||||
|
|
||||||
|
internal class WebSocketResponseProvider : IResponseProvider
|
||||||
|
{
|
||||||
|
private readonly WebSocketBuilder _builder;
|
||||||
|
|
||||||
|
public WebSocketResponseProvider(WebSocketBuilder builder)
|
||||||
|
{
|
||||||
|
_builder = Guard.NotNull(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(
|
||||||
|
IMapping mapping,
|
||||||
|
HttpContext context,
|
||||||
|
IRequestMessage requestMessage,
|
||||||
|
WireMockServerSettings settings)
|
||||||
|
{
|
||||||
|
// Check if this is a WebSocket upgrade request
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
return (ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Bad Request: Not a WebSocket upgrade request"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Accept the WebSocket connection
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var acceptContext = new WebSocketAcceptContext
|
||||||
|
{
|
||||||
|
SubProtocol = _builder.AcceptProtocol,
|
||||||
|
KeepAliveInterval = _builder.KeepAliveIntervalSeconds ?? TimeSpan.FromSeconds(WebSocketConstants.DefaultKeepAliveIntervalSeconds)
|
||||||
|
|
||||||
|
};
|
||||||
|
var webSocket = await context.WebSockets.AcceptWebSocketAsync(acceptContext).ConfigureAwait(false);
|
||||||
|
#else
|
||||||
|
var webSocket = await context.WebSockets.AcceptWebSocketAsync(_builder.AcceptProtocol).ConfigureAwait(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Get options from HttpContext.Items (set by WireMockMiddleware)
|
||||||
|
if (!context.Items.TryGetValue(nameof(WireMockMiddlewareOptions), out var optionsObj) ||
|
||||||
|
optionsObj is not IWireMockMiddlewareOptions options)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create registry from options (not from server)
|
||||||
|
var registry = _builder.IsBroadcast
|
||||||
|
? options.WebSocketRegistries.GetOrAdd(mapping.Guid, _ => new WebSocketConnectionRegistry())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Create WebSocket context
|
||||||
|
var wsContext = new WireMockWebSocketContext(
|
||||||
|
context,
|
||||||
|
webSocket,
|
||||||
|
requestMessage,
|
||||||
|
mapping,
|
||||||
|
registry,
|
||||||
|
_builder
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update scenario state following the same pattern as WireMockMiddleware
|
||||||
|
if (mapping.Scenario != null)
|
||||||
|
{
|
||||||
|
wsContext.UpdateScenarioState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to registry if broadcast is enabled
|
||||||
|
if (registry != null)
|
||||||
|
{
|
||||||
|
registry.AddConnection(wsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Handle the WebSocket based on configuration
|
||||||
|
if (_builder.ProxySettings != null)
|
||||||
|
{
|
||||||
|
await HandleProxyAsync(wsContext, _builder.ProxySettings).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (_builder.IsEcho)
|
||||||
|
{
|
||||||
|
await HandleEchoAsync(wsContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (_builder.MessageHandler != null)
|
||||||
|
{
|
||||||
|
await HandleCustomAsync(wsContext, _builder.MessageHandler).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (_builder.MessageSequence != null)
|
||||||
|
{
|
||||||
|
await HandleSequenceAsync(wsContext, _builder.MessageSequence).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default: keep connection open until client closes
|
||||||
|
await WaitForCloseAsync(wsContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Remove from registry
|
||||||
|
if (registry != null)
|
||||||
|
{
|
||||||
|
registry.RemoveConnection(wsContext.ConnectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return special marker to indicate WebSocket was handled
|
||||||
|
return (new WebSocketHandledResponse(), null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
settings.Logger?.Error($"WebSocket error for mapping '{mapping.Guid}': {ex.Message}", ex);
|
||||||
|
|
||||||
|
// If we haven't upgraded yet, we can return HTTP error
|
||||||
|
if (!context.Response.HasStarted)
|
||||||
|
{
|
||||||
|
return (ResponseMessageBuilder.Create(HttpStatusCode.InternalServerError, $"WebSocket error: {ex.Message}"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already upgraded - return marker
|
||||||
|
return (new WebSocketHandledResponse(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleEchoAsync(WireMockWebSocketContext context)
|
||||||
|
{
|
||||||
|
var bufferSize = context.Builder.MaxMessageSize ?? WebSocketConstants.DefaultReceiveBufferSize;
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
|
||||||
|
var cts = new CancellationTokenSource(timeout);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var result = await context.WebSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(buffer),
|
||||||
|
cts.Token
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(
|
||||||
|
WebSocketCloseStatus.NormalClosure,
|
||||||
|
"Closed by client"
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo back
|
||||||
|
await context.WebSocket.SendAsync(
|
||||||
|
new ArraySegment<byte>(buffer, 0, result.Count),
|
||||||
|
result.MessageType,
|
||||||
|
result.EndOfMessage,
|
||||||
|
cts.Token
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (context.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCustomAsync(
|
||||||
|
WireMockWebSocketContext context,
|
||||||
|
Func<WebSocketMessage, IWebSocketContext, Task> handler)
|
||||||
|
{
|
||||||
|
var bufferSize = context.Builder.MaxMessageSize ?? WebSocketConstants.DefaultReceiveBufferSize;
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
|
||||||
|
var cts = new CancellationTokenSource(timeout);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var result = await context.WebSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(buffer),
|
||||||
|
cts.Token
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(
|
||||||
|
WebSocketCloseStatus.NormalClosure,
|
||||||
|
"Closed by client"
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = CreateWebSocketMessage(result, buffer);
|
||||||
|
|
||||||
|
// Call custom handler
|
||||||
|
await handler(message, context).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (context.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSequenceAsync(WireMockWebSocketContext context, WebSocketMessageSequence sequence)
|
||||||
|
{
|
||||||
|
await sequence.ExecuteAsync(context).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleProxyAsync(WireMockWebSocketContext context, ProxyAndRecordSettings settings)
|
||||||
|
{
|
||||||
|
using var clientWebSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
var targetUri = new Uri(settings.Url);
|
||||||
|
await clientWebSocket.ConnectAsync(targetUri, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Bidirectional proxy
|
||||||
|
var clientToServer = ForwardMessagesAsync(context.WebSocket, clientWebSocket);
|
||||||
|
var serverToClient = ForwardMessagesAsync(clientWebSocket, context.WebSocket);
|
||||||
|
|
||||||
|
await Task.WhenAny(clientToServer, serverToClient).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Close both
|
||||||
|
if (context.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closed");
|
||||||
|
}
|
||||||
|
if (clientWebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closed", CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ForwardMessagesAsync(WebSocket source, WebSocket destination)
|
||||||
|
{
|
||||||
|
var buffer = new byte[WebSocketConstants.ProxyForwardBufferSize];
|
||||||
|
|
||||||
|
while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await destination.CloseAsync(
|
||||||
|
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
||||||
|
result.CloseStatusDescription,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await destination.SendAsync(
|
||||||
|
new ArraySegment<byte>(buffer, 0, result.Count),
|
||||||
|
result.MessageType,
|
||||||
|
result.EndOfMessage,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForCloseAsync(WireMockWebSocketContext context)
|
||||||
|
{
|
||||||
|
var buffer = new byte[WebSocketConstants.MinimumBufferSize];
|
||||||
|
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
|
||||||
|
var cts = new CancellationTokenSource(timeout);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var result = await context.WebSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(buffer),
|
||||||
|
cts.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (context.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebSocketMessage CreateWebSocketMessage(WebSocketReceiveResult result, byte[] buffer)
|
||||||
|
{
|
||||||
|
var message = new WebSocketMessage
|
||||||
|
{
|
||||||
|
MessageType = result.MessageType,
|
||||||
|
EndOfMessage = result.EndOfMessage,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
message.Text = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message.Bytes = new byte[result.Count];
|
||||||
|
Array.Copy(buffer, message.Bytes, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Special response marker to indicate WebSocket has been handled
|
||||||
|
/// </summary>
|
||||||
|
internal class WebSocketHandledResponse : ResponseMessage
|
||||||
|
{
|
||||||
|
public WebSocketHandledResponse()
|
||||||
|
{
|
||||||
|
// 101 Switching Protocols
|
||||||
|
StatusCode = (int)HttpStatusCode.SwitchingProtocols;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ public class MappingFileNameSanitizer
|
|||||||
if (!string.IsNullOrEmpty(mapping.Title))
|
if (!string.IsNullOrEmpty(mapping.Title))
|
||||||
{
|
{
|
||||||
// remove 'Proxy Mapping for ' and an extra space character after the HTTP request method
|
// remove 'Proxy Mapping for ' and an extra space character after the HTTP request method
|
||||||
name = mapping.Title.Replace(ProxyAndRecordSettings.DefaultPrefixForSavedMappingFile, "").Replace(' '.ToString(), string.Empty);
|
name = mapping.Title!.Replace(ProxyAndRecordSettings.DefaultPrefixForSavedMappingFile, "").Replace(' '.ToString(), string.Empty);
|
||||||
if (_settings.ProxyAndRecordSettings?.AppendGuidToSavedMappingFile == true)
|
if (_settings.ProxyAndRecordSettings?.AppendGuidToSavedMappingFile == true)
|
||||||
{
|
{
|
||||||
name += $"{ReplaceChar}{mapping.Guid}";
|
name += $"{ReplaceChar}{mapping.Guid}";
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
|||||||
private int _timesInSameState = 1;
|
private int _timesInSameState = 1;
|
||||||
private bool? _useWebhookFireAndForget;
|
private bool? _useWebhookFireAndForget;
|
||||||
private double? _probability;
|
private double? _probability;
|
||||||
private GraphQLSchemaDetails? _graphQLSchemaDetails;
|
private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
|
||||||
|
|
||||||
public Guid Guid { get; private set; }
|
public Guid Guid { get; private set; }
|
||||||
|
|
||||||
@@ -79,6 +79,12 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void RespondWith(IResponseProvider provider)
|
public void RespondWith(IResponseProvider provider)
|
||||||
{
|
{
|
||||||
|
if (provider is Response response && response.WebSocketBuilder != null)
|
||||||
|
{
|
||||||
|
// If the provider is a Response with a WebSocketBuilder, we need to use a WebSocketResponseProvider instead.
|
||||||
|
provider = new WebSocketResponseProvider(response.WebSocketBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
var mapping = new Mapping
|
var mapping = new Mapping
|
||||||
(
|
(
|
||||||
Guid,
|
Guid,
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ public partial class WireMockServer
|
|||||||
/// Checks if file exists.
|
/// Checks if file exists.
|
||||||
/// Note: Response is returned with no body as a head request doesn't accept a body, only the status code.
|
/// Note: Response is returned with no body as a head request doesn't accept a body, only the status code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestMessage">The request message.</param>
|
|
||||||
private IResponseMessage FileHead(HttpContext _, IRequestMessage requestMessage)
|
private IResponseMessage FileHead(HttpContext _, IRequestMessage requestMessage)
|
||||||
{
|
{
|
||||||
var filename = GetFileNameFromRequestMessage(requestMessage);
|
var filename = GetFileNameFromRequestMessage(requestMessage);
|
||||||
|
|||||||
77
src/WireMock.Net.Minimal/Server/WireMockServer.WebSocket.cs
Normal file
77
src/WireMock.Net.Minimal/Server/WireMockServer.WebSocket.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using WireMock.WebSockets;
|
||||||
|
|
||||||
|
namespace WireMock.Server;
|
||||||
|
|
||||||
|
public partial class WireMockServer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get all active WebSocket connections
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyCollection<WireMockWebSocketContext> GetWebSocketConnections()
|
||||||
|
{
|
||||||
|
return _options.WebSocketRegistries.Values
|
||||||
|
.SelectMany(r => r.GetConnections())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get WebSocket connections for a specific mapping
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyCollection<WireMockWebSocketContext> GetWebSocketConnections(Guid mappingGuid)
|
||||||
|
{
|
||||||
|
return _options.WebSocketRegistries.TryGetValue(mappingGuid, out var registry) ? registry.GetConnections() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close a specific WebSocket connection
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public async Task CloseWebSocketConnectionAsync(
|
||||||
|
Guid connectionId,
|
||||||
|
WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure,
|
||||||
|
string statusDescription = "Closed by server")
|
||||||
|
{
|
||||||
|
foreach (var registry in _options.WebSocketRegistries.Values)
|
||||||
|
{
|
||||||
|
if (registry.TryGetConnection(connectionId, out var connection))
|
||||||
|
{
|
||||||
|
await connection.CloseAsync(closeStatus, statusDescription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast a message to all WebSocket connections in a specific mapping
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public async Task BroadcastToWebSocketsAsync(Guid mappingGuid, string text)
|
||||||
|
{
|
||||||
|
if (_options.WebSocketRegistries.TryGetValue(mappingGuid, out var registry))
|
||||||
|
{
|
||||||
|
await registry.BroadcastTextAsync(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast a message to all WebSocket connections
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public async Task BroadcastToAllWebSocketsAsync(string text)
|
||||||
|
{
|
||||||
|
foreach (var registry in _options.WebSocketRegistries.Values)
|
||||||
|
{
|
||||||
|
await registry.BroadcastTextAsync(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Stef.Validation;
|
using Stef.Validation;
|
||||||
|
using WireMock.Constants;
|
||||||
using WireMock.Logging;
|
using WireMock.Logging;
|
||||||
using WireMock.Models;
|
using WireMock.Models;
|
||||||
using WireMock.Types;
|
using WireMock.Types;
|
||||||
@@ -86,6 +87,7 @@ public static class WireMockServerSettingsParser
|
|||||||
ParseCertificateSettings(settings, parser);
|
ParseCertificateSettings(settings, parser);
|
||||||
ParseHandlebarsSettings(settings, parser);
|
ParseHandlebarsSettings(settings, parser);
|
||||||
ParseActivityTracingSettings(settings, parser);
|
ParseActivityTracingSettings(settings, parser);
|
||||||
|
ParseWebSocketSettings(settings, parser);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -242,4 +244,20 @@ public static class WireMockServerSettingsParser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ParseWebSocketSettings(WireMockServerSettings settings, SimpleSettingsParser parser)
|
||||||
|
{
|
||||||
|
// Check if any WebSocket setting is present
|
||||||
|
if (parser.ContainsAny(
|
||||||
|
nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.MaxConnections),
|
||||||
|
nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.KeepAliveIntervalSeconds))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
settings.WebSocketSettings = new WebSocketSettings
|
||||||
|
{
|
||||||
|
MaxConnections = parser.GetIntValue(nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.MaxConnections), 100),
|
||||||
|
KeepAliveIntervalSeconds = parser.GetIntValue(nameof(WebSocketSettings) + '.' + nameof(WebSocketSettings.KeepAliveIntervalSeconds), WebSocketConstants.DefaultKeepAliveIntervalSeconds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright © WireMock.Net
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Nelibur.ObjectMapper;
|
using Nelibur.ObjectMapper;
|
||||||
using WireMock.Admin.Mappings;
|
using WireMock.Admin.Mappings;
|
||||||
using WireMock.Admin.Settings;
|
using WireMock.Admin.Settings;
|
||||||
@@ -7,6 +8,7 @@ using WireMock.Settings;
|
|||||||
|
|
||||||
namespace WireMock.Util;
|
namespace WireMock.Util;
|
||||||
|
|
||||||
|
[SuppressMessage("Performance", "CA1822:Mark members as static")]
|
||||||
internal sealed class TinyMapperUtils
|
internal sealed class TinyMapperUtils
|
||||||
{
|
{
|
||||||
public static TinyMapperUtils Instance { get; } = new();
|
public static TinyMapperUtils Instance { get; } = new();
|
||||||
@@ -22,6 +24,9 @@ internal sealed class TinyMapperUtils
|
|||||||
TinyMapper.Bind<WebProxySettingsModel, WebProxySettings>();
|
TinyMapper.Bind<WebProxySettingsModel, WebProxySettings>();
|
||||||
TinyMapper.Bind<WebProxyModel, WebProxySettings>();
|
TinyMapper.Bind<WebProxyModel, WebProxySettings>();
|
||||||
TinyMapper.Bind<ProxyUrlReplaceSettingsModel, ProxyUrlReplaceSettings>();
|
TinyMapper.Bind<ProxyUrlReplaceSettingsModel, ProxyUrlReplaceSettings>();
|
||||||
|
|
||||||
|
TinyMapper.Bind<WebSocketSettings, WebSocketSettingsModel>();
|
||||||
|
TinyMapper.Bind<WebSocketSettingsModel, WebSocketSettings>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance)
|
public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance)
|
||||||
@@ -53,4 +58,14 @@ internal sealed class TinyMapperUtils
|
|||||||
{
|
{
|
||||||
return model == null ? null : TinyMapper.Map<WebProxySettings>(model);
|
return model == null ? null : TinyMapper.Map<WebProxySettings>(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WebSocketSettingsModel? Map(WebSocketSettings? instance)
|
||||||
|
{
|
||||||
|
return instance == null ? null : TinyMapper.Map<WebSocketSettingsModel>(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebSocketSettings? Map(WebSocketSettingsModel? model)
|
||||||
|
{
|
||||||
|
return model == null ? null : TinyMapper.Map<WebSocketSettings>(model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
133
src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
Normal file
133
src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Stef.Validation;
|
||||||
|
using WireMock.Settings;
|
||||||
|
using WireMock.Types;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
internal class WebSocketBuilder : IWebSocketBuilder
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? AcceptProtocol { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEcho { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsBroadcast { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Func<WebSocketMessage, IWebSocketContext, Task>? MessageHandler { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public WebSocketMessageSequence? MessageSequence { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ProxyAndRecordSettings? ProxySettings { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TimeSpan? CloseTimeout { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int? MaxMessageSize { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int? ReceiveBufferSize { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TimeSpan? KeepAliveIntervalSeconds { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool UseTransformer { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TransformerType TransformerType { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool UseTransformerForBodyAsFile { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ReplaceNodeOptions TransformerReplaceNodeOptions { get; private set; }
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithAcceptProtocol(string protocol)
|
||||||
|
{
|
||||||
|
AcceptProtocol = Guard.NotNull(protocol);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithEcho()
|
||||||
|
{
|
||||||
|
IsEcho = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithMessageHandler(
|
||||||
|
Func<WebSocketMessage, IWebSocketContext, Task> handler)
|
||||||
|
{
|
||||||
|
MessageHandler = Guard.NotNull(handler);
|
||||||
|
IsEcho = false; // Disable echo if custom handler is set
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithMessageSequence(
|
||||||
|
Action<IWebSocketMessageSequenceBuilder> configure)
|
||||||
|
{
|
||||||
|
var sequenceBuilder = new WebSocketMessageSequenceBuilder();
|
||||||
|
configure(sequenceBuilder);
|
||||||
|
MessageSequence = sequenceBuilder.Build();
|
||||||
|
IsEcho = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithBroadcast()
|
||||||
|
{
|
||||||
|
IsBroadcast = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithProxy(ProxyAndRecordSettings settings)
|
||||||
|
{
|
||||||
|
ProxySettings = Guard.NotNull(settings);
|
||||||
|
IsEcho = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithCloseTimeout(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
CloseTimeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithMaxMessageSize(int sizeInBytes)
|
||||||
|
{
|
||||||
|
MaxMessageSize = Guard.Condition(sizeInBytes, s => s > 0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithReceiveBufferSize(int sizeInBytes)
|
||||||
|
{
|
||||||
|
ReceiveBufferSize = Guard.Condition(sizeInBytes, s => s > 0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithKeepAliveInterval(TimeSpan interval)
|
||||||
|
{
|
||||||
|
KeepAliveIntervalSeconds = interval;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebSocketBuilder WithTransformer(
|
||||||
|
TransformerType transformerType = TransformerType.Handlebars,
|
||||||
|
bool useTransformerForBodyAsFile = false,
|
||||||
|
ReplaceNodeOptions transformerReplaceNodeOptions = ReplaceNodeOptions.EvaluateAndTryToConvert)
|
||||||
|
{
|
||||||
|
UseTransformer = true;
|
||||||
|
TransformerType = transformerType;
|
||||||
|
UseTransformerForBodyAsFile = useTransformerForBodyAsFile;
|
||||||
|
TransformerReplaceNodeOptions = transformerReplaceNodeOptions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry for managing WebSocket connections per mapping
|
||||||
|
/// </summary>
|
||||||
|
internal class WebSocketConnectionRegistry
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<Guid, WireMockWebSocketContext> _connections = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a connection to the registry
|
||||||
|
/// </summary>
|
||||||
|
public void AddConnection(WireMockWebSocketContext context)
|
||||||
|
{
|
||||||
|
_connections.TryAdd(context.ConnectionId, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a connection from the registry
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveConnection(Guid connectionId)
|
||||||
|
{
|
||||||
|
_connections.TryRemove(connectionId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all connections
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<WireMockWebSocketContext> GetConnections()
|
||||||
|
{
|
||||||
|
return _connections.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to get a specific connection
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetConnection(Guid connectionId, [NotNullWhen(true)] out WireMockWebSocketContext? connection)
|
||||||
|
{
|
||||||
|
return _connections.TryGetValue(connectionId, out connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast text to all connections
|
||||||
|
/// </summary>
|
||||||
|
public async Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tasks = _connections.Values
|
||||||
|
.Where(c => c.WebSocket.State == WebSocketState.Open)
|
||||||
|
.Select(c => c.SendTextAsync(text, cancellationToken));
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast JSON to all connections
|
||||||
|
/// </summary>
|
||||||
|
public async Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data);
|
||||||
|
await BroadcastTextAsync(json, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
// Placeholder classes for future implementation
|
||||||
|
internal class WebSocketMessageSequence
|
||||||
|
{
|
||||||
|
public Task ExecuteAsync(WireMockWebSocketContext context)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
internal class WebSocketMessageSequenceBuilder : IWebSocketMessageSequenceBuilder
|
||||||
|
{
|
||||||
|
public WebSocketMessageSequence Build()
|
||||||
|
{
|
||||||
|
return new WebSocketMessageSequence();
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
Normal file
195
src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Stef.Validation;
|
||||||
|
using WireMock.Owin;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket context implementation
|
||||||
|
/// </summary>
|
||||||
|
public class WireMockWebSocketContext : IWebSocketContext
|
||||||
|
{
|
||||||
|
private readonly IWireMockMiddlewareOptions _options;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid ConnectionId { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public HttpContext HttpContext { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public WebSocket WebSocket { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRequestMessage RequestMessage { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IMapping Mapping { get; }
|
||||||
|
|
||||||
|
internal WebSocketConnectionRegistry? Registry { get; }
|
||||||
|
internal WebSocketBuilder Builder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new WebSocketContext
|
||||||
|
/// </summary>
|
||||||
|
internal WireMockWebSocketContext(
|
||||||
|
HttpContext httpContext,
|
||||||
|
WebSocket webSocket,
|
||||||
|
IRequestMessage requestMessage,
|
||||||
|
IMapping mapping,
|
||||||
|
WebSocketConnectionRegistry? registry,
|
||||||
|
WebSocketBuilder builder)
|
||||||
|
{
|
||||||
|
HttpContext = Guard.NotNull(httpContext);
|
||||||
|
WebSocket = Guard.NotNull(webSocket);
|
||||||
|
RequestMessage = Guard.NotNull(requestMessage);
|
||||||
|
Mapping = Guard.NotNull(mapping);
|
||||||
|
Registry = registry;
|
||||||
|
Builder = Guard.NotNull(builder);
|
||||||
|
|
||||||
|
// Get options from HttpContext
|
||||||
|
if (httpContext.Items.TryGetValue("WireMockMiddlewareOptions", out var options))
|
||||||
|
{
|
||||||
|
_options = (IWireMockMiddlewareOptions)options!;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendTextAsync(string text, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(text);
|
||||||
|
return WebSocket.SendAsync(
|
||||||
|
new ArraySegment<byte>(bytes),
|
||||||
|
WebSocketMessageType.Text,
|
||||||
|
true,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendBytesAsync(byte[] bytes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return WebSocket.SendAsync(
|
||||||
|
new ArraySegment<byte>(bytes),
|
||||||
|
WebSocketMessageType.Binary,
|
||||||
|
true,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendJsonAsync(object data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data);
|
||||||
|
return SendTextAsync(json, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription)
|
||||||
|
{
|
||||||
|
return WebSocket.CloseAsync(closeStatus, statusDescription, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetScenarioState(string nextState)
|
||||||
|
{
|
||||||
|
SetScenarioState(nextState, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetScenarioState(string nextState, string? description)
|
||||||
|
{
|
||||||
|
if (Mapping.Scenario == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the same logic as WireMockMiddleware
|
||||||
|
if (_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenarioState))
|
||||||
|
{
|
||||||
|
// Directly set the next state (bypass counter logic for manual WebSocket state changes)
|
||||||
|
scenarioState.NextState = nextState;
|
||||||
|
scenarioState.Started = true;
|
||||||
|
scenarioState.Finished = nextState == null;
|
||||||
|
|
||||||
|
// Reset counter when manually setting state
|
||||||
|
scenarioState.Counter = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new scenario state if it doesn't exist
|
||||||
|
_options.Scenarios.TryAdd(Mapping.Scenario, new ScenarioState
|
||||||
|
{
|
||||||
|
Name = Mapping.Scenario,
|
||||||
|
NextState = nextState,
|
||||||
|
Started = true,
|
||||||
|
Finished = nextState == null,
|
||||||
|
Counter = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update scenario state following the same pattern as WireMockMiddleware.UpdateScenarioState
|
||||||
|
/// This is called automatically when the WebSocket connection is established.
|
||||||
|
/// </summary>
|
||||||
|
internal void UpdateScenarioState()
|
||||||
|
{
|
||||||
|
if (Mapping.Scenario == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure scenario exists
|
||||||
|
if (!_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow exact same logic as WireMockMiddleware.UpdateScenarioState
|
||||||
|
// Increase the number of times this state has been executed
|
||||||
|
scenario.Counter++;
|
||||||
|
|
||||||
|
// Only if the number of times this state is executed equals the required StateTimes,
|
||||||
|
// proceed to next state and reset the counter to 0
|
||||||
|
if (scenario.Counter == (Mapping.TimesInSameState ?? 1))
|
||||||
|
{
|
||||||
|
scenario.NextState = Mapping.NextState;
|
||||||
|
scenario.Counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else just update Started and Finished
|
||||||
|
scenario.Started = true;
|
||||||
|
scenario.Finished = Mapping.NextState == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (Registry != null)
|
||||||
|
{
|
||||||
|
await Registry.BroadcastTextAsync(text, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (Registry != null)
|
||||||
|
{
|
||||||
|
await Registry.BroadcastJsonAsync(data, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48'">
|
<ItemGroup Condition=" '$(TargetFramework)' == 'net48'">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.3.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.3.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// Copyright © WireMock.Net
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
using WireMock.Matchers;
|
using WireMock.Matchers;
|
||||||
using WireMock.Matchers.Request;
|
|
||||||
|
|
||||||
namespace WireMock.RequestBuilders;
|
namespace WireMock.RequestBuilders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The HttpVersionBuilder interface.
|
/// The HttpVersionBuilder interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IHttpVersionBuilder : IRequestMatcher
|
public interface IHttpVersionBuilder : IWebSocketRequestBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WithHttpVersion
|
/// WithHttpVersion
|
||||||
|
|||||||
15
src/WireMock.Net.Shared/RequestBuilders/IWebSocketBuilder.cs
Normal file
15
src/WireMock.Net.Shared/RequestBuilders/IWebSocketBuilder.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
using WireMock.Matchers.Request;
|
||||||
|
|
||||||
|
namespace WireMock.RequestBuilders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The BodyRequestBuilder interface.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketRequestBuilder : IRequestMatcher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Match WebSocket upgrade with optional protocols.
|
||||||
|
/// </summary>
|
||||||
|
IRequestBuilder WithWebSocketUpgrade(params string[] protocols);
|
||||||
|
}
|
||||||
@@ -3,14 +3,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using WireMock.ResponseProviders;
|
|
||||||
|
|
||||||
namespace WireMock.ResponseBuilders;
|
namespace WireMock.ResponseBuilders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The CallbackResponseBuilder interface.
|
/// The CallbackResponseBuilder interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICallbackResponseBuilder : IResponseProvider
|
public interface ICallbackResponseBuilder : IWebSocketResponseBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The callback builder
|
/// The callback builder
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using WireMock.ResponseProviders;
|
||||||
|
using WireMock.Settings;
|
||||||
|
using WireMock.WebSockets;
|
||||||
|
|
||||||
|
namespace WireMock.ResponseBuilders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The WebSocketResponseBuilder interface.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketResponseBuilder : IResponseProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configure WebSocket response behavior
|
||||||
|
/// </summary>
|
||||||
|
IResponseBuilder WithWebSocket(Action<IWebSocketBuilder> configure);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy WebSocket to another server
|
||||||
|
/// </summary>
|
||||||
|
IResponseBuilder WithWebSocketProxy(string targetUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy WebSocket to another server with settings
|
||||||
|
/// </summary>
|
||||||
|
IResponseBuilder WithWebSocketProxy(ProxyAndRecordSettings settings);
|
||||||
|
}
|
||||||
21
src/WireMock.Net.Shared/Settings/WebSocketSettings.cs
Normal file
21
src/WireMock.Net.Shared/Settings/WebSocketSettings.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using WireMock.Constants;
|
||||||
|
|
||||||
|
namespace WireMock.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket-specific settings
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of concurrent WebSocket connections (default: 100)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxConnections { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default keep-alive interval (default: 30 seconds)
|
||||||
|
/// </summary>
|
||||||
|
public int KeepAliveIntervalSeconds { get; set; } = WebSocketConstants.DefaultKeepAliveIntervalSeconds;
|
||||||
|
}
|
||||||
@@ -346,4 +346,10 @@ public class WireMockServerSettings
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
public ActivityTracingOptions? ActivityTracingOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket settings.
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
public WebSocketSettings? WebSocketSettings { get; set; }
|
||||||
}
|
}
|
||||||
84
src/WireMock.Net.Shared/WebSockets/IWebSocketBuilder.cs
Normal file
84
src/WireMock.Net.Shared/WebSockets/IWebSocketBuilder.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using WireMock.Settings;
|
||||||
|
using WireMock.Types;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket Response Builder interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Accept the WebSocket with a specific protocol
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithAcceptProtocol(string protocol);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Echo all received messages back to client
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithEcho();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle incoming WebSocket messages
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithMessageHandler(Func<WebSocketMessage, IWebSocketContext, Task> handler);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Define a sequence of messages to send
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithMessageSequence(Action<IWebSocketMessageSequenceBuilder> configure);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable broadcast mode for this mapping
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithBroadcast();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy to another WebSocket server
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithProxy(ProxyAndRecordSettings settings);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set close timeout (default: 10 minutes)
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithCloseTimeout(TimeSpan timeout);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set maximum message size in bytes (default: 1 MB)
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithMaxMessageSize(int sizeInBytes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set receive buffer size (default: 4096 bytes)
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithReceiveBufferSize(int sizeInBytes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set keep-alive interval (default: 30 seconds)
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithKeepAliveInterval(TimeSpan interval);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable transformer support (Handlebars/Scriban)
|
||||||
|
/// </summary>
|
||||||
|
[PublicAPI]
|
||||||
|
IWebSocketBuilder WithTransformer(
|
||||||
|
TransformerType transformerType = TransformerType.Handlebars,
|
||||||
|
bool useTransformerForBodyAsFile = false,
|
||||||
|
ReplaceNodeOptions transformerReplaceNodeOptions = ReplaceNodeOptions.EvaluateAndTryToConvert);
|
||||||
|
}
|
||||||
85
src/WireMock.Net.Shared/WebSockets/IWebSocketContext.cs
Normal file
85
src/WireMock.Net.Shared/WebSockets/IWebSocketContext.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket context interface for handling WebSocket connections
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique connection identifier
|
||||||
|
/// </summary>
|
||||||
|
Guid ConnectionId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ASP.NET Core HttpContext
|
||||||
|
/// </summary>
|
||||||
|
HttpContext HttpContext { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The WebSocket instance
|
||||||
|
/// </summary>
|
||||||
|
WebSocket WebSocket { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The original request that initiated the WebSocket connection
|
||||||
|
/// </summary>
|
||||||
|
IRequestMessage RequestMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mapping that matched this WebSocket request
|
||||||
|
/// </summary>
|
||||||
|
IMapping Mapping { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send text message to the client
|
||||||
|
/// </summary>
|
||||||
|
Task SendTextAsync(string text, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send binary message to the client
|
||||||
|
/// </summary>
|
||||||
|
Task SendBytesAsync(byte[] bytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send JSON message to the client
|
||||||
|
/// </summary>
|
||||||
|
Task SendJsonAsync(object data, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close the WebSocket connection
|
||||||
|
/// </summary>
|
||||||
|
Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manually set the scenario state. This bypasses the counter logic and directly sets the next state.
|
||||||
|
/// Use this for programmatic state changes during WebSocket sessions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nextState">The next state to transition to</param>
|
||||||
|
void SetScenarioState(string nextState);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manually set the scenario state with description. This bypasses the counter logic and directly sets the next state.
|
||||||
|
/// Use this for programmatic state changes during WebSocket sessions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nextState">The next state to transition to</param>
|
||||||
|
/// <param name="description">Optional description for logging</param>
|
||||||
|
void SetScenarioState(string nextState, string? description);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast text message to all connections in this mapping
|
||||||
|
/// </summary>
|
||||||
|
Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast JSON message to all connections in this mapping
|
||||||
|
/// </summary>
|
||||||
|
Task BroadcastJsonAsync(object data, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket Message Sequence Builder interface (placeholder for future implementation)
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketMessageSequenceBuilder
|
||||||
|
{
|
||||||
|
// Future: Methods for building message sequences
|
||||||
|
}
|
||||||
37
src/WireMock.Net.Shared/WebSockets/WebSocketMessage.cs
Normal file
37
src/WireMock.Net.Shared/WebSockets/WebSocketMessage.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace WireMock.WebSockets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a WebSocket message
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The message type (Text or Binary)
|
||||||
|
/// </summary>
|
||||||
|
public WebSocketMessageType MessageType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text content (when MessageType is Text)
|
||||||
|
/// </summary>
|
||||||
|
public string? Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binary content (when MessageType is Binary)
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? Bytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether this is the final message
|
||||||
|
/// </summary>
|
||||||
|
public bool EndOfMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp when the message was received
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user