Skip to content

Commit ca5e3d3

Browse files
committed
TASK-039-006: Split + Renumber TurboHttp.Tests/RFC9113/
Split 9 oversized test files (>500 lines) into 20 files and renumbered all RFC9113 test files consecutively 01–39 with no gaps. Splits: - 01_ConnectionPrefaceTests (965 lines) → 3 parts - 02_FrameParsingTests (562 lines) → 2 parts - 06_HeadersTests (621 lines) → 2 parts - 09_ContinuationFrameTests (574 lines) → 2 parts - 21_FuzzHarnessTests (574 lines) → 2 parts - 22_SettingsMaxConcurrentTests (801 lines) → 3 parts - 23_ResourceExhaustionTests (597 lines) → 2 parts - 24_HighConcurrencyTests (553 lines) → 2 parts - 25_CrossComponentValidationTests (571 lines) → 2 parts All 39 files ≤ 500 lines. Build: 0 errors, 0 warnings. Tests: 567 passed, 0 failed.
1 parent 64adc23 commit ca5e3d3

42 files changed

Lines changed: 4195 additions & 3273 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.maggus/features/feature_039.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,11 @@ rmdir src/TurboHttp.Tests/Configuration
310310
**Renumbering:** After all splits, renumber the entire `RFC9113/` folder consecutively starting at `01_`.
311311

312312
**Acceptance Criteria:**
313-
- [ ] All files in `src/TurboHttp.Tests/RFC9113/` are ≤ 500 lines
314-
- [ ] Files are numbered consecutively with no gaps
315-
- [ ] `dotnet build --configuration Release ./src/TurboHttp.sln` succeeds
316-
- [ ] `dotnet test --project src/TurboHttp.Tests/TurboHttp.Tests.csproj -- --filter-namespace "TurboHttp.Tests.RFC9113"` passes with same test count
317-
- [ ] Unit tests are written and successful
313+
- [x] All files in `src/TurboHttp.Tests/RFC9113/` are ≤ 500 lines
314+
- [x] Files are numbered consecutively with no gaps (01–39)
315+
- [x] `dotnet build --configuration Release ./src/TurboHttp.sln` succeeds (0 warnings, 0 errors)
316+
- [x] `dotnet test --project src/TurboHttp.Tests/TurboHttp.Tests.csproj -- --filter-namespace "TurboHttp.Tests.RFC9113"` passes with same test count (567)
317+
- [x] Unit tests are written and successful
318318

319319
---
320320

src/TurboHttp.Tests/RFC9113/01_ConnectionPrefacePart1Tests.cs

Lines changed: 334 additions & 0 deletions
Large diffs are not rendered by default.

src/TurboHttp.Tests/RFC9113/01_ConnectionPrefaceTests.cs

Lines changed: 0 additions & 966 deletions
This file was deleted.

src/TurboHttp.Tests/RFC9113/02_ConnectionPrefacePart2Tests.cs

Lines changed: 371 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
using System.Net;
2+
using TurboHttp.Protocol.RFC7541;
3+
using TurboHttp.Protocol.RFC9113;
4+
5+
namespace TurboHttp.Tests.RFC9113;
6+
7+
/// <summary>
8+
/// Tests HTTP/2 connection preface encoding and decoding per RFC 9113 §3.4/3.5.
9+
/// Part 3: HEADERS frame, CONTINUATION frame.
10+
/// </summary>
11+
/// <remarks>
12+
/// Class under test: <see cref="Http2FrameDecoder"/>.
13+
/// </remarks>
14+
public sealed class Http2ConnectionPrefacePart3Tests
15+
{
16+
// RFC 9113 §3.4: client connection preface = magic octets + SETTINGS frame
17+
private static readonly byte[] Magic = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"u8.ToArray();
18+
private const int MagicLength = 24; // "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
19+
private const int FrameHeaderLength = 9;
20+
21+
// Helper: Decode server responses from frame bytes (replaces Http2ProtocolSession.Responses)
22+
private static List<(int StreamId, HttpResponseMessage Response)> DecodeResponses(ReadOnlyMemory<byte> data)
23+
{
24+
var decoder = new Http2FrameDecoder();
25+
var hpack = new HpackDecoder();
26+
var frames = decoder.Decode(data);
27+
var responses = new List<(int, HttpResponseMessage)>();
28+
var pending = new Dictionary<int, (HttpResponseMessage Response, List<byte> Body)>();
29+
30+
foreach (var frame in frames)
31+
{
32+
switch (frame)
33+
{
34+
case HeadersFrame { EndHeaders: true } h:
35+
{
36+
var hdrs = hpack.Decode(h.HeaderBlockFragment.Span);
37+
var resp = BuildResponseFromHpack(hdrs);
38+
if (resp == null) break;
39+
if (h.EndStream) responses.Add((h.StreamId, resp));
40+
else pending[h.StreamId] = (resp, []);
41+
break;
42+
}
43+
case DataFrame d:
44+
if (pending.TryGetValue(d.StreamId, out var p))
45+
p.Body.AddRange(d.Data.ToArray());
46+
if (d.EndStream && pending.TryGetValue(d.StreamId, out var completed))
47+
{
48+
completed.Response.Content = new ByteArrayContent(completed.Body.ToArray());
49+
responses.Add((d.StreamId, completed.Response));
50+
pending.Remove(d.StreamId);
51+
}
52+
53+
break;
54+
}
55+
}
56+
57+
return responses;
58+
}
59+
60+
private static HttpResponseMessage? BuildResponseFromHpack(IReadOnlyList<HpackHeader> headers)
61+
{
62+
var statusHeader = headers.FirstOrDefault(h => h.Name == ":status");
63+
if (statusHeader == default || !int.TryParse(statusHeader.Value, out var code)) return null;
64+
var response = new HttpResponseMessage((HttpStatusCode)code);
65+
foreach (var h in headers.Where(h => !h.Name.StartsWith(':')))
66+
{
67+
if (!response.Headers.TryAddWithoutValidation(h.Name, h.Value))
68+
{
69+
response.Content.Headers.TryAddWithoutValidation(h.Name, h.Value);
70+
}
71+
}
72+
73+
return response;
74+
}
75+
76+
// HEADERS frame tests (migrated from 12_DecoderConnectionPrefaceTests — Phase 6)
77+
78+
[Fact(DisplayName = "RFC9113-6.2-001: HEADERS frame decoded into response headers")]
79+
public void Should_DecodeResponseHeaders_When_HeadersFrameReceived()
80+
{
81+
var hpack = new HpackEncoder(useHuffman: false);
82+
var headerBlock = hpack.Encode([(":status", "200"), ("x-custom", "value")]);
83+
var frame = new HeadersFrame(1, headerBlock, endStream: true, endHeaders: true).Serialize();
84+
85+
var responses = DecodeResponses(frame.AsMemory());
86+
87+
Assert.Single(responses);
88+
var response = responses[0].Response;
89+
Assert.Equal(200, (int)response.StatusCode);
90+
Assert.True(response.Headers.Contains("x-custom"));
91+
}
92+
93+
[Fact(DisplayName = "RFC9113-6.2-002: END_STREAM on HEADERS closes stream immediately")]
94+
public void Should_CloseStreamImmediately_When_HeadersFrameHasEndStream()
95+
{
96+
var hpack = new HpackEncoder(useHuffman: false);
97+
var headerBlock = hpack.Encode([(":status", "204")]);
98+
var frame = new HeadersFrame(1, headerBlock, endStream: true, endHeaders: true).Serialize();
99+
100+
var responses = DecodeResponses(frame.AsMemory());
101+
102+
Assert.Single(responses);
103+
Assert.Equal(204, (int)responses[0].Response.StatusCode);
104+
}
105+
106+
[Fact(DisplayName = "RFC9113-6.2-003: END_HEADERS on HEADERS marks complete block")]
107+
public void Should_CompleteHeaderBlock_When_HeadersFrameHasEndHeaders()
108+
{
109+
var hpack = new HpackEncoder(useHuffman: false);
110+
var headerBlock = hpack.Encode([(":status", "200")]);
111+
var frame = new HeadersFrame(1, headerBlock, endStream: false, endHeaders: true).Serialize();
112+
113+
var decoder = new Http2FrameDecoder();
114+
decoder.Decode(frame);
115+
116+
// If END_HEADERS was respected, a subsequent non-CONTINUATION frame must not throw.
117+
var pingFrame = new PingFrame(new byte[8]).Serialize();
118+
var frames = decoder.Decode(pingFrame);
119+
Assert.Single(frames); // no exception — END_HEADERS was recognised
120+
}
121+
122+
[Fact(DisplayName = "RFC9113-6.2-004: Padded HEADERS padding stripped")]
123+
public void Should_StripPadding_When_HeadersFrameIsPadded()
124+
{
125+
var hpack = new HpackEncoder(useHuffman: false);
126+
var headerBlock = hpack.Encode([(":status", "200")]);
127+
128+
// Build PADDED HEADERS: PADDED flag=0x08, pad_length=2, header block, 2 bytes padding.
129+
const int padLength = 2;
130+
var payload = new byte[1 + headerBlock.Length + padLength];
131+
payload[0] = padLength; // Pad Length
132+
headerBlock.CopyTo(payload.AsMemory(1));
133+
// last 2 bytes remain zero (padding)
134+
135+
var frame = new byte[9 + payload.Length];
136+
frame[0] = 0;
137+
frame[1] = 0;
138+
frame[2] = (byte)payload.Length;
139+
frame[3] = 0x01; // HEADERS
140+
frame[4] = 0x0D; // END_STREAM(0x1) | END_HEADERS(0x4) | PADDED(0x8)
141+
frame[5] = 0;
142+
frame[6] = 0;
143+
frame[7] = 0;
144+
frame[8] = 1; // stream=1
145+
payload.CopyTo(frame, 9);
146+
147+
var responses = DecodeResponses(frame.AsMemory());
148+
149+
Assert.Single(responses);
150+
Assert.Equal(200, (int)responses[0].Response.StatusCode);
151+
}
152+
153+
[Fact(DisplayName = "RFC9113-6.2-005: PRIORITY flag in HEADERS consumed correctly")]
154+
public void Should_ConsumePriorityFlagCorrectly_When_HeadersFrameHasPriorityFlag()
155+
{
156+
var hpack = new HpackEncoder(useHuffman: false);
157+
var headerBlock = hpack.Encode([(":status", "200")]);
158+
159+
// Build HEADERS with PRIORITY flag: 5 extra bytes (4 stream dep + 1 weight).
160+
var priorityBytes = new byte[] { 0x00, 0x00, 0x00, 0x03, 0x0F }; // dep=3, weight=15
161+
var payload = priorityBytes.Concat(headerBlock.ToArray()).ToArray();
162+
163+
var frame = new byte[9 + payload.Length];
164+
frame[0] = 0;
165+
frame[1] = 0;
166+
frame[2] = (byte)payload.Length;
167+
frame[3] = 0x01; // HEADERS
168+
frame[4] = 0x25; // END_STREAM(0x1) | END_HEADERS(0x4) | PRIORITY(0x20)
169+
frame[5] = 0;
170+
frame[6] = 0;
171+
frame[7] = 0;
172+
frame[8] = 1; // stream=1
173+
payload.CopyTo(frame, 9);
174+
175+
var responses = DecodeResponses(frame.AsMemory());
176+
177+
Assert.Single(responses);
178+
Assert.Equal(200, (int)responses[0].Response.StatusCode);
179+
}
180+
181+
[Fact(DisplayName = "RFC9113-6.2-006: HEADERS without END_HEADERS waits for CONTINUATION")]
182+
public void Should_WaitForContinuation_When_HeadersFrameLacksEndHeaders()
183+
{
184+
var hpack = new HpackEncoder(useHuffman: false);
185+
var headerBlock = hpack.Encode([(":status", "200")]);
186+
var split1 = headerBlock[..(headerBlock.Length / 2)];
187+
var split2 = headerBlock[(headerBlock.Length / 2)..];
188+
189+
var headersFrame = new HeadersFrame(1, split1, endStream: true, endHeaders: false).Serialize();
190+
var contFrame = new ContinuationFrame(1, split2, endHeaders: true).Serialize();
191+
192+
// Use same decoder instance to maintain state
193+
var decoder = new Http2FrameDecoder();
194+
var frames1 = decoder.Decode(headersFrame);
195+
Assert.Single(frames1); // HEADERS frame is decoded
196+
197+
// Decoder is now awaiting CONTINUATION (continuation state maintained by decoder)
198+
var frames2 = decoder.Decode(contFrame);
199+
Assert.Single(frames2); // CONTINUATION frame is decoded
200+
}
201+
202+
[Fact(DisplayName = "RFC9113-6.2-007: HEADERS on stream 0 is PROTOCOL_ERROR")]
203+
public void Should_ThrowProtocolError_When_HeadersFrameOnStream0()
204+
{
205+
// RFC 9113 §6.2: HEADERS on stream 0 is a connection error.
206+
// Http2FrameDecoder parses the frame; stream=0 validation happens at session layer.
207+
var frame = new byte[]
208+
{
209+
0x00, 0x00, 0x01,
210+
0x01, 0x05,
211+
0x00, 0x00, 0x00, 0x00, // stream=0
212+
0x88
213+
};
214+
var decoder = new Http2FrameDecoder();
215+
// Decoder parses the frame itself without validation of stream ID constraints
216+
var frames = decoder.Decode(frame);
217+
var headersFrame = Assert.IsType<HeadersFrame>(Assert.Single(frames));
218+
Assert.Equal(0, headersFrame.StreamId);
219+
}
220+
221+
// CONTINUATION frame tests (migrated from 12_DecoderConnectionPrefaceTests — Phase 6)
222+
223+
[Fact(DisplayName = "RFC9113-6.9-001: CONTINUATION appended to HEADERS block")]
224+
public void Should_MergeHeaderBlock_When_ContinuationFrameAppendedToHeaders()
225+
{
226+
var hpack = new HpackEncoder(useHuffman: false);
227+
var headerBlock = hpack.Encode([(":status", "200"), ("x-test", "cont")]);
228+
var split = headerBlock.Length / 2;
229+
230+
var headersFrame = new HeadersFrame(1, headerBlock[..split], endStream: true, endHeaders: false).Serialize();
231+
var contFrame = new ContinuationFrame(1, headerBlock[split..], endHeaders: true).Serialize();
232+
233+
// Use same decoder instance to maintain continuation state
234+
var decoder = new Http2FrameDecoder();
235+
decoder.Decode(headersFrame);
236+
237+
var frames = decoder.Decode(contFrame);
238+
Assert.Single(frames);
239+
var cont = Assert.IsType<ContinuationFrame>(frames[0]);
240+
Assert.True(cont.EndHeaders);
241+
}
242+
243+
[Fact(DisplayName = "RFC9113-6.9-dec-002: END_HEADERS on final CONTINUATION completes block")]
244+
public void Should_CompleteBlock_When_ContinuationFrameHasEndHeaders()
245+
{
246+
var hpack = new HpackEncoder(useHuffman: false);
247+
var headerBlock = hpack.Encode([(":status", "200")]);
248+
249+
var headersFrame = new HeadersFrame(1, headerBlock[..1], endStream: true, endHeaders: false).Serialize();
250+
var contFrame = new ContinuationFrame(1, headerBlock[1..], endHeaders: true).Serialize();
251+
252+
var decoder = new Http2FrameDecoder();
253+
decoder.Decode(headersFrame);
254+
var frames = decoder.Decode(contFrame);
255+
256+
Assert.Single(frames);
257+
var cont = Assert.IsType<ContinuationFrame>(frames[0]);
258+
Assert.True(cont.EndHeaders);
259+
}
260+
261+
[Fact(DisplayName = "RFC9113-6.9-003: Multiple CONTINUATION frames all merged")]
262+
public void Should_MergeAll_When_MultipleContinuationFrames()
263+
{
264+
var hpack = new HpackEncoder(useHuffman: false);
265+
var headerBlock = hpack.Encode([(":status", "200"), ("a", "1"), ("b", "2"), ("c", "3")]);
266+
var third = headerBlock.Length / 3;
267+
268+
var headersFrame = new HeadersFrame(1, headerBlock[..third], endStream: true, endHeaders: false).Serialize();
269+
var cont1 = new ContinuationFrame(1, headerBlock[third..(2 * third)], endHeaders: false).Serialize();
270+
var cont2 = new ContinuationFrame(1, headerBlock[(2 * third)..], endHeaders: true).Serialize();
271+
272+
var decoder = new Http2FrameDecoder();
273+
decoder.Decode(headersFrame);
274+
decoder.Decode(cont1);
275+
var frames = decoder.Decode(cont2);
276+
277+
Assert.Single(frames);
278+
var cont = Assert.IsType<ContinuationFrame>(frames[0]);
279+
Assert.True(cont.EndHeaders);
280+
}
281+
282+
[Fact(DisplayName = "RFC9113-6.9-004: CONTINUATION on wrong stream is PROTOCOL_ERROR")]
283+
public void Should_ThrowProtocolError_When_ContinuationFrameOnWrongStream()
284+
{
285+
var headersFrame = new byte[]
286+
{
287+
0x00, 0x00, 0x01,
288+
0x01, 0x00,
289+
0x00, 0x00, 0x00, 0x01, // stream=1
290+
0x82
291+
};
292+
var contFrame = new byte[]
293+
{
294+
0x00, 0x00, 0x01,
295+
0x09, 0x04,
296+
0x00, 0x00, 0x00, 0x03, // stream=3 (wrong)
297+
0x84
298+
};
299+
300+
var combined = headersFrame.Concat(contFrame).ToArray();
301+
var decoder = new Http2FrameDecoder();
302+
var ex = Assert.Throws<Http2Exception>(() => decoder.Decode(combined));
303+
Assert.Equal(Http2ErrorCode.ProtocolError, ex.ErrorCode);
304+
}
305+
306+
[Fact(DisplayName = "RFC9113-6.9-005: Non-CONTINUATION after HEADERS is PROTOCOL_ERROR")]
307+
public void Should_ThrowProtocolError_When_NonContinuationFollowsHeadersWithoutEndHeaders()
308+
{
309+
// RFC 9113 §6.9: After HEADERS without END_HEADERS, next frame MUST be CONTINUATION.
310+
// Http2FrameDecoder enforces this and throws PROTOCOL_ERROR if violated.
311+
var hpack = new HpackEncoder(useHuffman: false);
312+
var headerBlock = hpack.Encode([(":status", "200")]);
313+
var headersFrame = new HeadersFrame(1, headerBlock, endStream: false, endHeaders: false).Serialize();
314+
var pingFrame = new PingFrame(new byte[8]).Serialize();
315+
316+
var decoder = new Http2FrameDecoder();
317+
decoder.Decode(headersFrame);
318+
319+
// PING while awaiting CONTINUATION must be PROTOCOL_ERROR.
320+
var ex = Assert.Throws<Http2Exception>(() => decoder.Decode(pingFrame));
321+
Assert.Equal(Http2ErrorCode.ProtocolError, ex.ErrorCode);
322+
}
323+
324+
[Fact(DisplayName = "RFC9113-6.9-006: CONTINUATION on stream 0 is PROTOCOL_ERROR")]
325+
public void Should_ThrowProtocolError_When_ContinuationFrameOnStream0()
326+
{
327+
var headersOnStream1 = new byte[]
328+
{
329+
0x00, 0x00, 0x01,
330+
0x01, 0x00,
331+
0x00, 0x00, 0x00, 0x01, // stream=1
332+
0x82
333+
};
334+
var contOnStream0 = new byte[]
335+
{
336+
0x00, 0x00, 0x01,
337+
0x09, 0x04,
338+
0x00, 0x00, 0x00, 0x00, // stream=0
339+
0x84
340+
};
341+
342+
var combined = headersOnStream1.Concat(contOnStream0).ToArray();
343+
var decoder = new Http2FrameDecoder();
344+
var ex = Assert.Throws<Http2Exception>(() => decoder.Decode(combined));
345+
Assert.Equal(Http2ErrorCode.ProtocolError, ex.ErrorCode);
346+
}
347+
348+
[Fact(DisplayName = "RFC9113-6.10-CONT-001: CONTINUATION without HEADERS is PROTOCOL_ERROR")]
349+
public void Should_ThrowProtocolError_When_ContinuationFrameHasNoPrecedingHeaders()
350+
{
351+
var contFrame = new ContinuationFrame(1, new byte[] { 0x88 }, endHeaders: true).Serialize();
352+
var decoder = new Http2FrameDecoder();
353+
var ex = Assert.Throws<Http2Exception>(() => decoder.Decode(contFrame));
354+
Assert.Equal(Http2ErrorCode.ProtocolError, ex.ErrorCode);
355+
}
356+
}

0 commit comments

Comments
 (0)