Skip to content

Commit 980eeef

Browse files
committed
# TASK-023-008: Custom Handler Pipeline Tests – H11 + H2
Implements integration tests for the custom handler pipeline (`TurboHandler`, `UseRequest()`, `UseResponse()`, `AddHandler<T>()`). ## Changes - `src/TurboHttp.IntegrationTests/H11/HandlerPipelineIntegrationTests.cs` (NEW) - 8 tests: Handler-001 through Handler-008 - Covers UseRequest injection, UseResponse mutation, AddHandler<T> typed handler, multi-handler ordering, original-request capture, redirect pipeline, compression pipeline, and cookie+handler interaction - `src/TurboHttp.IntegrationTests/H2/HandlerPipelineIntegrationTests.cs` (NEW) - 4 tests: Handler-H2-001, Handler-H2-003, Handler-H2-004, Handler-H2-006 - Protocol-agnostic handler verification over HTTP/2 cleartext - `src/TurboHttp.IntegrationTests/Shared/Routes.cs` - Added `/interaction/echo-all-headers` route: echoes X-* request headers as response headers and Cookie header as `X-Received-Cookie` (required for Handler-008 cookie+handler test) - `.maggus/features/feature_023.md` - Checked all TASK-023-008 acceptance criteria ## Test Results - H11: 8/8 pass - H2: 4/4 pass - Build: 0 errors, 0 warnings
1 parent 99c1bb5 commit 980eeef

4 files changed

Lines changed: 403 additions & 7 deletions

File tree

.maggus/features/feature_023.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,13 @@ public static ITurboHttpClientBuilder WithExpectContinue(
296296
**HTTP/2 Variant (4 Tests):** Tests 1, 3, 4, 6 over HTTP/2 to verify that handlers are protocol-agnostic.
297297

298298
**Acceptance Criteria:**
299-
- [ ] `/H11/HandlerPipelineIntegrationTests.cs` created (HTTP/1.1, 8 tests)
300-
- [ ] `/H2/HandlerPipelineIntegrationTests.cs` created (HTTP/2, 4 selected tests)
301-
- [ ] Custom `TestHeaderHandler : TurboHandler` class defined in test file
302-
- [ ] Tests use `configure: b => b.UseRequest(...)`, `b.UseResponse(...)`, `b.AddHandler<T>()`
303-
- [ ] DisplayNames follow `Handler-001` / `Handler-H2-001`
304-
- [ ] All 12 tests pass
305-
- [ ] Build passes with zero warnings
299+
- [x] `/H11/HandlerPipelineIntegrationTests.cs` created (HTTP/1.1, 8 tests)
300+
- [x] `/H2/HandlerPipelineIntegrationTests.cs` created (HTTP/2, 4 selected tests)
301+
- [x] Custom `TestHeaderHandler : TurboHandler` class defined in test file
302+
- [x] Tests use `configure: b => b.UseRequest(...)`, `b.UseResponse(...)`, `b.AddHandler<T>()`
303+
- [x] DisplayNames follow `Handler-001` / `Handler-H2-001`
304+
- [x] All 12 tests pass
305+
- [x] Build passes with zero warnings
306306

307307
**Files:**
308308
- `src/TurboHttp.IntegrationTests/H11/HandlerPipelineIntegrationTests.cs` (NEW)
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
using System.Net;
2+
using TurboHttp.IntegrationTests.Shared;
3+
using TurboHttp.Protocol.RFC6265;
4+
using TurboHttp.Protocol.RFC9110;
5+
6+
namespace TurboHttp.IntegrationTests.H11;
7+
8+
[Collection("H11")]
9+
public sealed class HandlerPipelineIntegrationTests
10+
{
11+
private readonly ServerFixture _server;
12+
private readonly ActorSystemFixture _systemFixture;
13+
14+
public HandlerPipelineIntegrationTests(ServerFixture server, ActorSystemFixture systemFixture)
15+
{
16+
_server = server;
17+
_systemFixture = systemFixture;
18+
}
19+
20+
private sealed class TestHeaderHandler : TurboHandler
21+
{
22+
public override HttpRequestMessage ProcessRequest(HttpRequestMessage request)
23+
{
24+
request.Headers.TryAddWithoutValidation("X-Typed-Handler", "active");
25+
return request;
26+
}
27+
}
28+
29+
[Fact(DisplayName = "Handler-001: UseRequest injects custom header")]
30+
public async Task UseRequest_Injects_Custom_Header()
31+
{
32+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
33+
await using var helper = ClientHelper.CreateClient(
34+
_server.HttpPort,
35+
new Version(1, 1),
36+
configure: b => b.UseRequest(req =>
37+
{
38+
req.Headers.TryAddWithoutValidation("X-Custom-Injected", "hello");
39+
return req;
40+
}),
41+
system: _systemFixture.System);
42+
43+
var response = await helper.Client.SendAsync(
44+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
45+
46+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
47+
Assert.True(
48+
response.Headers.TryGetValues("X-Custom-Injected", out var vals),
49+
"X-Custom-Injected header must be echoed back");
50+
Assert.Equal("hello", string.Join(",", vals));
51+
}
52+
53+
[Fact(DisplayName = "Handler-002: UseResponse adds header to response")]
54+
public async Task UseResponse_Adds_Header_To_Response()
55+
{
56+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
57+
await using var helper = ClientHelper.CreateClient(
58+
_server.HttpPort,
59+
new Version(1, 1),
60+
configure: b => b.UseResponse((_, res) =>
61+
{
62+
res.Headers.TryAddWithoutValidation("X-Handler-Added", "injected");
63+
return res;
64+
}),
65+
system: _systemFixture.System);
66+
67+
var response = await helper.Client.SendAsync(
68+
new HttpRequestMessage(HttpMethod.Get, "/hello"), cts.Token);
69+
70+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
71+
Assert.True(
72+
response.Headers.TryGetValues("X-Handler-Added", out var vals),
73+
"X-Handler-Added header must be present on the response");
74+
Assert.Equal("injected", string.Join(",", vals));
75+
}
76+
77+
[Fact(DisplayName = "Handler-003: AddHandler typed handler processes request")]
78+
public async Task AddHandler_Typed_Handler_Processes_Request()
79+
{
80+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
81+
await using var helper = ClientHelper.CreateClient(
82+
_server.HttpPort,
83+
new Version(1, 1),
84+
configure: b => b.AddHandler<TestHeaderHandler>(),
85+
system: _systemFixture.System);
86+
87+
var response = await helper.Client.SendAsync(
88+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
89+
90+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
91+
Assert.True(
92+
response.Headers.TryGetValues("X-Typed-Handler", out var vals),
93+
"X-Typed-Handler header must be echoed back");
94+
Assert.Equal("active", string.Join(",", vals));
95+
}
96+
97+
[Fact(DisplayName = "Handler-004: Multiple handlers execute in registration order")]
98+
public async Task Multiple_Handlers_Execute_In_Registration_Order()
99+
{
100+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
101+
await using var helper = ClientHelper.CreateClient(
102+
_server.HttpPort,
103+
new Version(1, 1),
104+
configure: b => b
105+
.UseRequest(req =>
106+
{
107+
req.Headers.TryAddWithoutValidation("X-First", "1");
108+
return req;
109+
})
110+
.UseRequest(req =>
111+
{
112+
req.Headers.TryAddWithoutValidation("X-Second", "2");
113+
return req;
114+
}),
115+
system: _systemFixture.System);
116+
117+
var response = await helper.Client.SendAsync(
118+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
119+
120+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
121+
Assert.True(response.Headers.TryGetValues("X-First", out var firstVals), "X-First must be present");
122+
Assert.True(response.Headers.TryGetValues("X-Second", out var secondVals), "X-Second must be present");
123+
Assert.Equal("1", string.Join(",", firstVals));
124+
Assert.Equal("2", string.Join(",", secondVals));
125+
}
126+
127+
[Fact(DisplayName = "Handler-005: Handler sees original request on response")]
128+
public async Task Handler_Sees_Original_Request_On_Response()
129+
{
130+
string? capturedOriginalUrl = null;
131+
132+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
133+
await using var helper = ClientHelper.CreateClient(
134+
_server.HttpPort,
135+
new Version(1, 1),
136+
configure: b => b.UseResponse((original, res) =>
137+
{
138+
capturedOriginalUrl = original.RequestUri?.PathAndQuery;
139+
return res;
140+
}),
141+
system: _systemFixture.System);
142+
143+
var response = await helper.Client.SendAsync(
144+
new HttpRequestMessage(HttpMethod.Get, "/hello"), cts.Token);
145+
146+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
147+
Assert.Equal("/hello", capturedOriginalUrl);
148+
}
149+
150+
[Fact(DisplayName = "Handler-006: Handler works with redirect pipeline")]
151+
public async Task Handler_Works_With_Redirect_Pipeline()
152+
{
153+
// Handler injects X-Handler-Redirect → 302 → /headers/echo
154+
// Redirect stage forwards the original request (with injected headers) to the new URL.
155+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
156+
await using var helper = ClientHelper.CreateClient(
157+
_server.HttpPort,
158+
new Version(1, 1),
159+
configure: b => b
160+
.UseRequest(req =>
161+
{
162+
req.Headers.TryAddWithoutValidation("X-Handler-Redirect", "present");
163+
return req;
164+
})
165+
.WithRedirect(),
166+
system: _systemFixture.System);
167+
168+
// /redirect/302/headers/echo → 302 → /headers/echo which echoes request headers
169+
var response = await helper.Client.SendAsync(
170+
new HttpRequestMessage(HttpMethod.Get, "/redirect/302/headers/echo"), cts.Token);
171+
172+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
173+
Assert.True(
174+
response.Headers.TryGetValues("X-Handler-Redirect", out var vals),
175+
"X-Handler-Redirect must still be present after redirect");
176+
Assert.Equal("present", string.Join(",", vals));
177+
}
178+
179+
[Fact(DisplayName = "Handler-007: Handler works with compression pipeline")]
180+
public async Task Handler_Works_With_Compression_Pipeline()
181+
{
182+
// UseResponse handler receives a response after decompression stage has run.
183+
// The handler should see a normal (decompressed) response.
184+
int? capturedContentLength = null;
185+
186+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
187+
await using var helper = ClientHelper.CreateClient(
188+
_server.HttpPort,
189+
new Version(1, 1),
190+
configure: b => b
191+
.WithDecompression()
192+
.UseResponse((_, res) =>
193+
{
194+
capturedContentLength = (int?)res.Content.Headers.ContentLength;
195+
return res;
196+
}),
197+
system: _systemFixture.System);
198+
199+
// /compress/gzip/1 → gzip-encoded 1 KB body; WithDecompression decompresses it
200+
var response = await helper.Client.SendAsync(
201+
new HttpRequestMessage(HttpMethod.Get, "/compress/gzip/1"), cts.Token);
202+
203+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
204+
var body = await response.Content.ReadAsByteArrayAsync(cts.Token);
205+
Assert.Equal(1024, body.Length);
206+
}
207+
208+
[Fact(DisplayName = "Handler-008: Handler works with cookie pipeline")]
209+
public async Task Handler_Works_With_Cookie_Pipeline()
210+
{
211+
// UseRequest injects X-From-Handler.
212+
// WithCookies causes the jar to inject a Cookie header on subsequent requests.
213+
// /interaction/echo-all-headers echoes X-* headers AND Cookie as X-Received-Cookie.
214+
var jar = new CookieJar();
215+
216+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
217+
await using var helper = ClientHelper.CreateClient(
218+
_server.HttpPort,
219+
new Version(1, 1),
220+
configure: b => b
221+
.UseRequest(req =>
222+
{
223+
req.Headers.TryAddWithoutValidation("X-From-Handler", "yes");
224+
return req;
225+
})
226+
.WithCookies(jar),
227+
system: _systemFixture.System);
228+
229+
// Seed the jar with a cookie via the set endpoint
230+
var seedResponse = await helper.Client.SendAsync(
231+
new HttpRequestMessage(HttpMethod.Get, "/cookie/set/testcookie/testvalue"), cts.Token);
232+
Assert.Equal(HttpStatusCode.OK, seedResponse.StatusCode);
233+
234+
// Now both the handler header and the cookie should be present
235+
var response = await helper.Client.SendAsync(
236+
new HttpRequestMessage(HttpMethod.Get, "/interaction/echo-all-headers"), cts.Token);
237+
238+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
239+
Assert.True(
240+
response.Headers.TryGetValues("X-From-Handler", out var handlerVals),
241+
"X-From-Handler must be echoed back");
242+
Assert.Equal("yes", string.Join(",", handlerVals));
243+
Assert.True(
244+
response.Headers.TryGetValues("X-Received-Cookie", out var cookieVals),
245+
"X-Received-Cookie must be present (cookie jar injected cookie)");
246+
Assert.Contains("testcookie=testvalue", string.Join(",", cookieVals));
247+
}
248+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Net;
2+
using TurboHttp.IntegrationTests.Shared;
3+
4+
namespace TurboHttp.IntegrationTests.H2;
5+
6+
[Collection("H2")]
7+
public sealed class HandlerPipelineIntegrationTests
8+
{
9+
private readonly ServerFixture _server;
10+
private readonly ActorSystemFixture _systemFixture;
11+
12+
public HandlerPipelineIntegrationTests(ServerFixture server, ActorSystemFixture systemFixture)
13+
{
14+
_server = server;
15+
_systemFixture = systemFixture;
16+
}
17+
18+
private sealed class TestHeaderHandler : TurboHandler
19+
{
20+
public override HttpRequestMessage ProcessRequest(HttpRequestMessage request)
21+
{
22+
request.Headers.TryAddWithoutValidation("X-Typed-Handler", "active");
23+
return request;
24+
}
25+
}
26+
27+
[Fact(DisplayName = "Handler-H2-001: UseRequest injects custom header")]
28+
public async Task UseRequest_Injects_Custom_Header()
29+
{
30+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
31+
await using var helper = ClientHelper.CreateClient(
32+
_server.H2Port,
33+
new Version(2, 0),
34+
configure: b => b.UseRequest(req =>
35+
{
36+
req.Headers.TryAddWithoutValidation("X-Custom-Injected", "hello");
37+
return req;
38+
}),
39+
system: _systemFixture.System);
40+
41+
var response = await helper.Client.SendAsync(
42+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
43+
44+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
45+
Assert.True(
46+
response.Headers.TryGetValues("X-Custom-Injected", out var vals),
47+
"X-Custom-Injected header must be echoed back");
48+
Assert.Equal("hello", string.Join(",", vals));
49+
}
50+
51+
[Fact(DisplayName = "Handler-H2-003: AddHandler typed handler processes request")]
52+
public async Task AddHandler_Typed_Handler_Processes_Request()
53+
{
54+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
55+
await using var helper = ClientHelper.CreateClient(
56+
_server.H2Port,
57+
new Version(2, 0),
58+
configure: b => b.AddHandler<TestHeaderHandler>(),
59+
system: _systemFixture.System);
60+
61+
var response = await helper.Client.SendAsync(
62+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
63+
64+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
65+
Assert.True(
66+
response.Headers.TryGetValues("X-Typed-Handler", out var vals),
67+
"X-Typed-Handler header must be echoed back");
68+
Assert.Equal("active", string.Join(",", vals));
69+
}
70+
71+
[Fact(DisplayName = "Handler-H2-004: Multiple handlers execute in registration order")]
72+
public async Task Multiple_Handlers_Execute_In_Registration_Order()
73+
{
74+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
75+
await using var helper = ClientHelper.CreateClient(
76+
_server.H2Port,
77+
new Version(2, 0),
78+
configure: b => b
79+
.UseRequest(req =>
80+
{
81+
req.Headers.TryAddWithoutValidation("X-First", "1");
82+
return req;
83+
})
84+
.UseRequest(req =>
85+
{
86+
req.Headers.TryAddWithoutValidation("X-Second", "2");
87+
return req;
88+
}),
89+
system: _systemFixture.System);
90+
91+
var response = await helper.Client.SendAsync(
92+
new HttpRequestMessage(HttpMethod.Get, "/headers/echo"), cts.Token);
93+
94+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
95+
Assert.True(response.Headers.TryGetValues("X-First", out var firstVals), "X-First must be present");
96+
Assert.True(response.Headers.TryGetValues("X-Second", out var secondVals), "X-Second must be present");
97+
Assert.Equal("1", string.Join(",", firstVals));
98+
Assert.Equal("2", string.Join(",", secondVals));
99+
}
100+
101+
[Fact(DisplayName = "Handler-H2-006: Handler works with redirect pipeline")]
102+
public async Task Handler_Works_With_Redirect_Pipeline()
103+
{
104+
// Handler injects X-Handler-Redirect → 302 → /headers/echo
105+
// Redirect stage forwards the original request (with injected headers) to the new URL.
106+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
107+
await using var helper = ClientHelper.CreateClient(
108+
_server.H2Port,
109+
new Version(2, 0),
110+
configure: b => b
111+
.UseRequest(req =>
112+
{
113+
req.Headers.TryAddWithoutValidation("X-Handler-Redirect", "present");
114+
return req;
115+
})
116+
.WithRedirect(),
117+
system: _systemFixture.System);
118+
119+
// /redirect/302/headers/echo → 302 → /headers/echo which echoes request headers
120+
var response = await helper.Client.SendAsync(
121+
new HttpRequestMessage(HttpMethod.Get, "/redirect/302/headers/echo"), cts.Token);
122+
123+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
124+
Assert.True(
125+
response.Headers.TryGetValues("X-Handler-Redirect", out var vals),
126+
"X-Handler-Redirect must still be present after redirect");
127+
Assert.Equal("present", string.Join(",", vals));
128+
}
129+
}

0 commit comments

Comments
 (0)