Skip to content

Commit c163c8c

Browse files
feat: Add WebSocket Protocol (#29)
* feat: Add WebSocket Protocol Signed-off-by: SebastienDegodez <sebastien.degodez@gmail.com> * docs: add note about WithAsyncFeature() Signed-off-by: SebastienDegodez <sebastien.degodez@gmail.com> --------- Signed-off-by: SebastienDegodez <sebastien.degodez@gmail.com>
1 parent 109d930 commit c163c8c

File tree

6 files changed

+194
-9
lines changed

6 files changed

+194
-9
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ You can create and build an ensemble that way:
163163

164164
```csharp
165165
MicrocksContainersEnsemble ensemble = new MicrocksContainerEnsemble(network, MicrocksImage)
166-
.WithMainArtifacts("pastry-orders-asyncapi.yml")
167-
.WithKafkaConnection(new KafkaConnection($"kafka:19092"));
166+
.WithMainArtifacts("pastry-orders-asyncapi.yml");
168167

169168
ensemble.StartAsync();
170169
```
@@ -176,15 +175,44 @@ You have to access it using:
176175
MicrocksContainer microcks = ensemble.MicrocksContainer;
177176
microcks.ImportAsMainArtifact(...);
178177
```
178+
179+
To activate async features (WebSocket), you can use `WithAsyncFeature()` method.
180+
181+
```csharp
182+
MicrocksContainersEnsemble ensemble = new MicrocksContainerEnsemble(network, MicrocksImage)
183+
.WithMainArtifacts("pastry-orders-asyncapi.yml")
184+
.WithAsyncFeature();
185+
186+
ensemble.StartAsync();
187+
```
188+
189+
#### Asynchronous API support
190+
Asynchronous API feature needs to be explicitly enabled as well.
191+
In case you want to use it for mocking purposes,
192+
you'll have to specify additional connection details to the broker of your choice.
193+
194+
To add a note indicating that it is not necessary to call `WithAsyncFeature()` when an additional method exists.
195+
196+
See an example below with connection to a Kafka broker:
197+
198+
```csharp
199+
MicrocksContainersEnsemble ensemble = new MicrocksContainerEnsemble(network, MicrocksImage)
200+
.WithMainArtifacts("pastry-orders-asyncapi.yml")
201+
.WithKafkaConnection(new KafkaConnection($"kafka:19092"));
202+
203+
ensemble.StartAsync();
204+
```
205+
179206
##### Using mock endpoints for your dependencies
180207

181208
Once started, the `ensemble.AsyncMinionContainer` provides methods for retrieving mock endpoint names for the different
182-
supported protocols (Kafka only at the moment).
209+
supported protocols (WebSocket, Kafka, etc. ...).
183210

184211
```csharp
185212
string kafkaTopic = ensemble.AsyncMinionContainer
186213
.GetKafkaMockTopic("Pastry orders API", "0.1.0", "SUBSCRIBE pastry/orders");
187214
```
215+
188216
##### Launching new contract-tests
189217

190218
Using contract-testing techniques on Asynchronous endpoints may require a different style of interacting with the Microcks

src/Microcks.Testcontainers/MicrocksAsyncMinionBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected override MicrocksAsyncMinionBuilder Init()
7575
.WithNetwork(this._network)
7676
.WithNetworkAliases("microcks-async-minion")
7777
.WithEnvironment("MICROCKS_HOST_PORT", "microcks:" + MicrocksBuilder.MicrocksHttpPort)
78-
.WithExposedPort(MicrocksAsyncMinionHttpPort)
78+
.WithPortBinding(MicrocksAsyncMinionHttpPort, true)
7979
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(".*Profile prod activated\\..*"));
8080
}
8181

src/Microcks.Testcontainers/MicrocksAsyncMinionContainer.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,39 @@ public MicrocksAsyncMinionContainer(MicrocksAsyncMinionConfiguration configurati
4141
/// <returns>A formatted Kafka mock topic name.</returns>
4242
public string GetKafkaMockTopic(string service, string version, string operationName)
4343
{
44-
// operationName may start with SUBSCRIBE or PUBLISH.
45-
if (operationName.Contains(' '))
46-
{
47-
operationName = operationName.Split(' ')[1];
48-
}
44+
operationName = ExtractOperationName(operationName);
45+
4946
return String.Format(DestinationPattern,
5047
service.Replace(" ", "").Replace("-", ""),
5148
version,
5249
operationName.Replace("/", "-"));
5350
}
51+
52+
/// <summary>
53+
/// Returns the WebSocket mock endpoint based on the provided service, version, and operation name.
54+
/// </summary>
55+
/// <returns>The WebSocket mock endpoint.</returns>
56+
public Uri GetWebSocketMockEndpoint(string service, string version, string operationName)
57+
{
58+
operationName = ExtractOperationName(operationName);
59+
var port = this.GetMappedPublicPort(MicrocksAsyncMinionBuilder.MicrocksAsyncMinionHttpPort);
60+
var escapedService = service.Replace(" ", "+");
61+
var escapedVersion = version.Replace(" ", "+");
62+
63+
return new Uri($"ws://{this.Hostname}:{port}/api/ws/{escapedService}/{escapedVersion}/{operationName}");
64+
}
65+
66+
/// <summary>
67+
/// Extracts the operation name from the provided operation name.
68+
/// </summary>
69+
/// <param name="operationName">operationName may start with SUBSCRIBE or PUBLISH.</param>
70+
/// <returns>The extracted operation name.</returns>
71+
private string ExtractOperationName(string operationName)
72+
{
73+
if (operationName.Contains(' '))
74+
{
75+
operationName = operationName.Split(' ')[1];
76+
}
77+
return operationName;
78+
}
5479
}

src/Microcks.Testcontainers/MicrocksContainer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,5 @@ protected override ValueTask DisposeAsyncCore()
122122
{
123123
return base.DisposeAsyncCore();
124124
}
125+
125126
}

src/Microcks.Testcontainers/MicrocksContainerEnsemble.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public class MicrocksContainerEnsemble : IAsyncDisposable
4343

4444
private readonly INetwork _network;
4545

46+
public INetwork Network { get => this._network; }
47+
48+
4649
private readonly string _microcksImage;
4750

4851
/// <summary>

tests/Microcks.Testcontainers.Tests/Async/MicrocksAsyncFeatureTest.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
//
1616
//
1717

18+
using System;
19+
using System.Linq;
20+
using System.Net.WebSockets;
21+
using System.Text;
22+
using System.Threading;
23+
using DotNet.Testcontainers.Builders;
24+
using DotNet.Testcontainers.Containers;
1825
using FluentAssertions;
26+
using Microcks.Testcontainers.Model;
1927

2028
namespace Microcks.Testcontainers.Tests.Async;
2129

@@ -26,19 +34,51 @@ public sealed class MicrocksAsyncFeatureTest : IAsyncLifetime
2634
/// </summary>
2735
private const string MicrocksImage = "quay.io/microcks/microcks-uber:1.10.1-native";
2836

37+
private const string BadPastryAsyncImage = "quay.io/microcks/contract-testing-demo-async:01";
38+
private const string GoodPastryAsyncImage = "quay.io/microcks/contract-testing-demo-async:02";
39+
2940
private MicrocksContainerEnsemble _microcksContainerEnsemble;
41+
private IContainer _wsGoodImplContainer;
42+
private IContainer _wsBadImplContainer;
3043

3144
public async Task DisposeAsync()
3245
{
3346
await this._microcksContainerEnsemble.DisposeAsync();
47+
await this._wsBadImplContainer.DisposeAsync();
48+
await this._wsGoodImplContainer.DisposeAsync();
3449
}
3550

3651
public async Task InitializeAsync()
3752
{
3853
this._microcksContainerEnsemble = new MicrocksContainerEnsemble(MicrocksImage)
54+
.WithMainArtifacts("pastry-orders-asyncapi.yml")
3955
.WithAsyncFeature();
4056

57+
this._wsBadImplContainer = new ContainerBuilder()
58+
.WithImage(BadPastryAsyncImage)
59+
.WithNetwork(this._microcksContainerEnsemble.Network)
60+
.WithNetworkAliases("bad-impl")
61+
.WithExposedPort(4001)
62+
.WithWaitStrategy(
63+
Wait.ForUnixContainer()
64+
.UntilMessageIsLogged(".*Starting WebSocket server on ws://localhost:4001/websocket.*")
65+
)
66+
.Build();
67+
68+
this._wsGoodImplContainer = new ContainerBuilder()
69+
.WithImage(GoodPastryAsyncImage)
70+
.WithNetwork(this._microcksContainerEnsemble.Network)
71+
.WithNetworkAliases("good-impl")
72+
.WithExposedPort(4002)
73+
.WithWaitStrategy(
74+
Wait.ForUnixContainer()
75+
.UntilMessageIsLogged(".*Starting WebSocket server on ws://localhost:4002/websocket.*")
76+
)
77+
.Build();
78+
4179
await this._microcksContainerEnsemble.StartAsync();
80+
await this._wsBadImplContainer.StartAsync();
81+
await this._wsGoodImplContainer.StartAsync();
4282
}
4383

4484
[Fact]
@@ -49,4 +89,92 @@ public void ShouldDetermineCorrectImageMessage()
4989
.Be("quay.io/microcks/microcks-uber-async-minion:1.10.1");
5090
}
5191

92+
/// <summary>
93+
/// Test method to verify that a WebSocket message is received when a message is emitted.
94+
/// </summary>
95+
/// <returns>A task that represents the asynchronous operation.</returns>
96+
[Fact]
97+
public async Task ShouldReceivedWebSocketMessageWhenMessageIsEmitted()
98+
{
99+
// Get the WebSocket endpoint for the "Pastry orders API" with version "0.1.0" and subscription "SUBSCRIBE pastry/orders".
100+
var webSocketEndpoint = _microcksContainerEnsemble
101+
.AsyncMinionContainer
102+
.GetWebSocketMockEndpoint("Pastry orders API" ,"0.1.0", "SUBSCRIBE pastry/orders");
103+
const string expectedMessage = "{\"id\":\"4dab240d-7847-4e25-8ef3-1530687650c8\",\"customerId\":\"fe1088b3-9f30-4dc1-a93d-7b74f0a072b9\",\"status\":\"VALIDATED\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}";
104+
105+
using var webSocketClient = new ClientWebSocket();
106+
await webSocketClient.ConnectAsync(webSocketEndpoint, CancellationToken.None);
107+
108+
var buffer = new byte[1024];
109+
110+
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(7));
111+
var result = await webSocketClient.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
112+
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
113+
114+
await webSocketClient.CloseAsync(
115+
WebSocketCloseStatus.NormalClosure,
116+
"Test done",
117+
CancellationToken.None);
118+
119+
message.Should().Be(expectedMessage);
120+
}
121+
122+
/// <summary>
123+
/// Test that verifies the correct status contract when a bad message is emitted.
124+
/// </summary>
125+
[Fact]
126+
public async Task ShouldReturnsCorrectStatusContractWhenBadMessageIsEmitted()
127+
{
128+
// New Test request
129+
var testRequest = new TestRequest
130+
{
131+
ServiceId = "Pastry orders API:0.1.0",
132+
RunnerType = TestRunnerType.ASYNC_API_SCHEMA,
133+
Timeout = TimeSpan.FromMilliseconds(70000),
134+
TestEndpoint = "ws://bad-impl:4001/websocket",
135+
};
136+
137+
var taskTestResult = _microcksContainerEnsemble.MicrocksContainer
138+
.TestEndpointAsync(testRequest);
139+
140+
var testResult = await taskTestResult;
141+
142+
// Assert
143+
testResult.InProgress.Should().Be(false);
144+
testResult.Success.Should().Be(false);
145+
testResult.TestedEndpoint.Should().Be(testRequest.TestEndpoint);
146+
147+
testResult.TestCaseResults.First().TestStepResults.Should().NotBeEmpty();
148+
var testStepResult = testResult.TestCaseResults.First().TestStepResults.First();
149+
testStepResult.Message.Should().Contain("object has missing required properties ([\"status\"]");
150+
}
151+
152+
/// <summary>
153+
/// Test that verifies the correct status contract when a good message is emitted.
154+
/// </summary>
155+
[Fact]
156+
public async Task ShouldReturnsCorrectStatusContractWhenGoodMessageIsEmitted()
157+
{
158+
// New Test request
159+
var testRequest = new TestRequest
160+
{
161+
ServiceId = "Pastry orders API:0.1.0",
162+
RunnerType = TestRunnerType.ASYNC_API_SCHEMA,
163+
Timeout = TimeSpan.FromMilliseconds(7000),
164+
TestEndpoint = "ws://good-impl:4002/websocket",
165+
};
166+
167+
var taskTestResult = _microcksContainerEnsemble.MicrocksContainer
168+
.TestEndpointAsync(testRequest);
169+
170+
var testResult = await taskTestResult;
171+
172+
// Assert
173+
testResult.InProgress.Should().Be(false);
174+
testResult.Success.Should().Be(true);
175+
testResult.TestedEndpoint.Should().Be(testRequest.TestEndpoint);
176+
177+
testResult.TestCaseResults.First().TestStepResults.Should().NotBeEmpty();
178+
testResult.TestCaseResults.First().TestStepResults.First().Message.Should().BeNullOrEmpty();
179+
}
52180
}

0 commit comments

Comments
 (0)