Skip to content

Commit 5756710

Browse files
ANcpLuaclaude
andcommitted
refactor: improve test reliability and fix analyzer warnings
Integration tests: - Migrate SseExtensionsTests to real HTTP integration (use TestServer.CreateClient) - Replace channel-based assertions with HTTP stream reading - Make assertions framework-agnostic (handle .NET 9 PascalCase vs .NET 10 camelCase) Publisher loops: - Replace infinite publish loops with bounded patterns - Wait for ClientCount > 0, publish once, eliminate background tasks - Simplify test cleanup (remove try/finally blocks) Analyzer warnings: - MA0036: Make SseExtensionsNet10Tests static - MA0158: Replace object lock with System.Threading.Lock in FakeCompletableSseStream Test Results: - .NET 9.0: 94/94 passed - .NET 10.0: 95/95 passed - Overall coverage: 100% lines (321/321), 98.39% branches (61/62) - SseExtensions: 100% line and branch coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9317f8a commit 5756710

File tree

4 files changed

+175
-206
lines changed

4 files changed

+175
-206
lines changed

SWEN3.Paperless.RabbitMq.Tests/Helpers/FakeCompletableSseStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal sealed class FakeCompletableSseStream<T> : ISseStream<T> where T : clas
1010
{
1111
private readonly Channel<T> _channel = Channel.CreateUnbounded<T>();
1212
private readonly Dictionary<Guid, ChannelReader<T>> _subscribers = new();
13-
private readonly object _lock = new();
13+
private readonly Lock _lock = new();
1414

1515
/// <summary>
1616
/// Gets the current number of active subscribers.

SWEN3.Paperless.RabbitMq.Tests/Integration/SseExtensionsTests.cs

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,33 @@ public async Task MapSse_WithPublishedEvent_ShouldStreamToClient()
1414

1515
using var _ = host;
1616
var sseStream = host.Services.GetRequiredService<ISseStream<Messages.SseTestEvent>>();
17+
var client = server.CreateClient();
18+
client.Timeout = Timeout.InfiniteTimeSpan;
19+
var ct = TestContext.Current.CancellationToken;
1720

18-
var clientId = Guid.NewGuid();
19-
var reader = sseStream.Subscribe(clientId);
21+
var responseTask = client.GetAsync(TestEndpoint, HttpCompletionOption.ResponseHeadersRead, ct);
22+
23+
// Wait for HTTP client to connect
24+
while (sseStream.ClientCount == 0)
25+
await Task.Delay(50, ct);
2026

2127
var testEvent = new Messages.SseTestEvent { Id = 42, Message = "Hello SSE" };
2228
sseStream.Publish(testEvent);
2329

24-
var result = await reader.ReadAsync(TestContext.Current.CancellationToken);
30+
using var response = await responseTask;
31+
response.EnsureSuccessStatusCode();
32+
await using var stream = await response.Content.ReadAsStreamAsync(ct);
33+
using var reader = new StreamReader(stream, Encoding.UTF8);
2534

26-
result.Id.Should().Be(42);
27-
result.Message.Should().Be("Hello SSE");
35+
var eventLine = await reader.ReadLineAsync(ct);
36+
var dataLine = await reader.ReadLineAsync(ct);
37+
var blankLine = await reader.ReadLineAsync(ct);
2838

29-
sseStream.Unsubscribe(clientId);
39+
eventLine.Should().Be("event: test-event");
40+
// Note: .NET 9 uses PascalCase, .NET 10+ uses camelCase
41+
dataLine.Should().Contain("42");
42+
dataLine.Should().Contain("Hello SSE");
43+
blankLine.Should().BeEmpty();
3044
}
3145

3246
[Fact]
@@ -39,25 +53,51 @@ public async Task MapSse_MultipleClients_ShouldReceiveSameEvent()
3953

4054
using var _ = host;
4155
var sseStream = host.Services.GetRequiredService<ISseStream<Messages.SseTestEvent>>();
56+
var ct = TestContext.Current.CancellationToken;
57+
58+
var client1 = server.CreateClient();
59+
var client2 = server.CreateClient();
60+
client1.Timeout = Timeout.InfiniteTimeSpan;
61+
client2.Timeout = Timeout.InfiniteTimeSpan;
4262

43-
var clientId1 = Guid.NewGuid();
44-
var clientId2 = Guid.NewGuid();
45-
var reader1 = sseStream.Subscribe(clientId1);
46-
var reader2 = sseStream.Subscribe(clientId2);
63+
var responseTask1 = client1.GetAsync(TestEndpoint, HttpCompletionOption.ResponseHeadersRead, ct);
64+
var responseTask2 = client2.GetAsync(TestEndpoint, HttpCompletionOption.ResponseHeadersRead, ct);
4765

48-
var readTask1 = reader1.ReadAsync(TestContext.Current.CancellationToken).AsTask();
49-
var readTask2 = reader2.ReadAsync(TestContext.Current.CancellationToken).AsTask();
66+
// Wait for both HTTP clients to connect
67+
while (sseStream.ClientCount < 2)
68+
await Task.Delay(50, ct);
5069

5170
var testEvent = new Messages.SseTestEvent { Id = 99, Message = "Broadcast" };
5271
sseStream.Publish(testEvent);
5372

54-
var result1 = await readTask1;
55-
var result2 = await readTask2;
73+
using var response1 = await responseTask1;
74+
using var response2 = await responseTask2;
75+
76+
response1.EnsureSuccessStatusCode();
77+
response2.EnsureSuccessStatusCode();
78+
79+
await using var stream1 = await response1.Content.ReadAsStreamAsync(ct);
80+
await using var stream2 = await response2.Content.ReadAsStreamAsync(ct);
81+
using var reader1 = new StreamReader(stream1, Encoding.UTF8);
82+
using var reader2 = new StreamReader(stream2, Encoding.UTF8);
83+
84+
var event1 = await reader1.ReadLineAsync(ct);
85+
var data1 = await reader1.ReadLineAsync(ct);
86+
var blank1 = await reader1.ReadLineAsync(ct);
87+
88+
var event2 = await reader2.ReadLineAsync(ct);
89+
var data2 = await reader2.ReadLineAsync(ct);
90+
var blank2 = await reader2.ReadLineAsync(ct);
5691

57-
result1.Should().BeEquivalentTo(testEvent);
58-
result2.Should().BeEquivalentTo(testEvent);
92+
event1.Should().Be("event: test-event");
93+
// Note: .NET 9 uses PascalCase, .NET 10+ uses camelCase
94+
data1.Should().Contain("99");
95+
data1.Should().Contain("Broadcast");
96+
blank1.Should().BeEmpty();
5997

60-
sseStream.Unsubscribe(clientId1);
61-
sseStream.Unsubscribe(clientId2);
98+
event2.Should().Be("event: test-event");
99+
data2.Should().Contain("99");
100+
data2.Should().Contain("Broadcast");
101+
blank2.Should().BeEmpty();
62102
}
63103
}

SWEN3.Paperless.RabbitMq.Tests/Unit/SseExtensionsFallbackTests.cs

Lines changed: 55 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,25 @@ public async Task MapSse_Fallback_ShouldWriteCorrectSseFormat()
2525
// Act
2626
var responseTask = client.GetAsync("/sse", HttpCompletionOption.ResponseHeadersRead, cts.Token);
2727

28-
// Publish repeatedly until the subscriber is definitely connected
29-
var publisherTask = Task.Run(async () =>
30-
{
31-
try
32-
{
33-
while (!cts.Token.IsCancellationRequested)
34-
{
35-
await Task.Delay(50, cts.Token);
36-
sseStream.Publish(new Messages.SseTestEvent { Id = 1, Message = "Hello" });
37-
}
38-
}
39-
catch (OperationCanceledException)
40-
{
41-
// Expected when the test ends
42-
}
43-
}, CancellationToken.None);
44-
45-
try
46-
{
47-
using var response = await responseTask;
48-
response.EnsureSuccessStatusCode();
49-
await using var stream = await response.Content.ReadAsStreamAsync(cts.Token);
50-
using var reader = new StreamReader(stream, Encoding.UTF8);
51-
52-
var line1 = await reader.ReadLineAsync(cts.Token);
53-
var line2 = await reader.ReadLineAsync(cts.Token);
54-
var line3 = await reader.ReadLineAsync(cts.Token);
55-
56-
line1.Should().Be("event: test-event");
57-
line2.Should().Be("data: {\"id\":1,\"msg\":\"Hello\"}");
58-
line3.Should().BeEmpty(); // The double newline
59-
}
60-
finally
61-
{
62-
await cts.CancelAsync();
63-
await publisherTask;
64-
}
28+
// Wait for client to connect
29+
while (sseStream.ClientCount == 0)
30+
await Task.Delay(50, cts.Token);
31+
32+
// Publish once
33+
sseStream.Publish(new Messages.SseTestEvent { Id = 1, Message = "Hello" });
34+
35+
using var response = await responseTask;
36+
response.EnsureSuccessStatusCode();
37+
await using var stream = await response.Content.ReadAsStreamAsync(cts.Token);
38+
using var reader = new StreamReader(stream, Encoding.UTF8);
39+
40+
var line1 = await reader.ReadLineAsync(cts.Token);
41+
var line2 = await reader.ReadLineAsync(cts.Token);
42+
var line3 = await reader.ReadLineAsync(cts.Token);
43+
44+
line1.Should().Be("event: test-event");
45+
line2.Should().Be("data: {\"id\":1,\"msg\":\"Hello\"}");
46+
line3.Should().BeEmpty(); // The double newline
6547
}
6648

6749
[Fact]
@@ -91,61 +73,44 @@ public async Task MapSse_Fallback_ValidatesHeadersAndMultiEventPayload()
9173
new Messages.SseTestEvent { Id = 2, Message = "Second" }
9274
};
9375

94-
var publisherTask = Task.Run(async () =>
95-
{
96-
try
97-
{
98-
while (!cts.Token.IsCancellationRequested)
99-
{
100-
await Task.Delay(50, cts.Token);
101-
foreach (var evt in events)
102-
{
103-
sseStream.Publish(evt);
104-
}
105-
}
106-
}
107-
catch (OperationCanceledException)
108-
{
109-
// Expected
110-
}
111-
}, CancellationToken.None);
112-
113-
try
114-
{
115-
using var response = await responseTask;
116-
117-
// Assert headers
118-
response.EnsureSuccessStatusCode();
119-
response.Headers.CacheControl?.NoCache.Should().BeTrue();
120-
response.Headers.Connection.Should().Contain("keep-alive");
121-
response.Content.Headers.ContentType?.MediaType.Should().Be("text/event-stream");
122-
123-
await using var stream = await response.Content.ReadAsStreamAsync(cts.Token);
124-
using var reader = new StreamReader(stream, Encoding.UTF8);
125-
126-
// Read first event
127-
var event1 = await reader.ReadLineAsync(cts.Token);
128-
var data1 = await reader.ReadLineAsync(cts.Token);
129-
var blank1 = await reader.ReadLineAsync(cts.Token);
130-
131-
event1.Should().Be("event: multi-event");
132-
data1.Should().Be("data: {\"id\":1,\"msg\":\"First\"}");
133-
blank1.Should().BeEmpty();
134-
135-
// Read second event
136-
var event2 = await reader.ReadLineAsync(cts.Token);
137-
var data2 = await reader.ReadLineAsync(cts.Token);
138-
var blank2 = await reader.ReadLineAsync(cts.Token);
139-
140-
event2.Should().Be("event: multi-event");
141-
data2.Should().Be("data: {\"id\":2,\"msg\":\"Second\"}");
142-
blank2.Should().BeEmpty();
143-
}
144-
finally
76+
// Wait for client to connect
77+
while (sseStream.ClientCount == 0)
78+
await Task.Delay(50, cts.Token);
79+
80+
// Publish events once
81+
foreach (var evt in events)
14582
{
146-
await cts.CancelAsync();
147-
await publisherTask;
83+
sseStream.Publish(evt);
14884
}
85+
86+
using var response = await responseTask;
87+
88+
// Assert headers
89+
response.EnsureSuccessStatusCode();
90+
response.Headers.CacheControl?.NoCache.Should().BeTrue();
91+
response.Headers.Connection.Should().Contain("keep-alive");
92+
response.Content.Headers.ContentType?.MediaType.Should().Be("text/event-stream");
93+
94+
await using var stream = await response.Content.ReadAsStreamAsync(cts.Token);
95+
using var reader = new StreamReader(stream, Encoding.UTF8);
96+
97+
// Read first event
98+
var event1 = await reader.ReadLineAsync(cts.Token);
99+
var data1 = await reader.ReadLineAsync(cts.Token);
100+
var blank1 = await reader.ReadLineAsync(cts.Token);
101+
102+
event1.Should().Be("event: multi-event");
103+
data1.Should().Be("data: {\"id\":1,\"msg\":\"First\"}");
104+
blank1.Should().BeEmpty();
105+
106+
// Read second event
107+
var event2 = await reader.ReadLineAsync(cts.Token);
108+
var data2 = await reader.ReadLineAsync(cts.Token);
109+
var blank2 = await reader.ReadLineAsync(cts.Token);
110+
111+
event2.Should().Be("event: multi-event");
112+
data2.Should().Be("data: {\"id\":2,\"msg\":\"Second\"}");
113+
blank2.Should().BeEmpty();
149114
}
150115

151116
[Fact]

0 commit comments

Comments
 (0)