lib/http/src/router/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const http = @import("../root.zig");
3 const time = @import("sys").time;
4 const sys = @import("sys").net;
5 const root = @import("root.zig");
6
7 const Router = root.Router;
8 const upgradeWebsocket = root.upgradeWebsocket;
9 const Connection = http.Connection;
10 const Request = http.Request;
11 const Response = http.Response;
12 const WebSocket = http.WebSocket;
13 const websocket_limits: http.WebSocketLimits = .{
14 .frame_payload_bytes = 4096,
15 .message_payload_bytes = 4096,
16 };
17
18 test "Router init and deinit" {
19 var ctx: u32 = 42;
20 var router = Router(*u32).init(std.testing.allocator, &ctx);
21 defer router.deinit();
22 }
23
24 const RouterTestAppState = struct {
25 counter: usize,
26 };
27
28 fn router_test_handler(_: *u32, _: *Request, response: *Response) anyerror!void {
29 response.status = 200;
30 response.body = "OK";
31 }
32
33 fn router_state_handler(state: *RouterTestAppState, _: *Request, response: *Response) anyerror!void {
34 state.counter += 1;
35 response.status = 200;
36 }
37
38 test "Router route registration" {
39 var ctx: u32 = 42;
40 var router = Router(*u32).init(std.testing.allocator, &ctx);
41 defer router.deinit();
42
43 try router.get("/health", router_test_handler);
44 try router.post("/api/data", router_test_handler);
45 try router.put("/api/data", router_test_handler);
46 try router.delete("/api/data", router_test_handler);
47 }
48
49 test "Router with app state context" {
50 var state = RouterTestAppState{ .counter = 0 };
51 var router = Router(*RouterTestAppState).init(std.testing.allocator, &state);
52 defer router.deinit();
53
54 try router.get("/increment", router_state_handler);
55 }
56
57 const TestState = struct {
58 one: bool = false,
59 two: bool = false,
60 hits: usize = 0,
61 };
62
63 fn testHandler(state: *TestState, req: *Request, res: *Response) anyerror!void {
64 state.hits += 1;
65 const path = req.pathOnly();
66 if (std.mem.eql(u8, path, "/one")) state.one = true;
67 if (std.mem.eql(u8, path, "/two")) state.two = true;
68 res.status = 200;
69 res.status_text = "OK";
70 res.body = path;
71 }
72
73 fn testHeadHandler(state: *TestState, _: *Request, res: *Response) anyerror!void {
74 state.hits += 1;
75 res.status = 200;
76 res.status_text = "OK";
77 res.body = "head body";
78 }
79
80 fn testNoContentHandler(state: *TestState, _: *Request, res: *Response) anyerror!void {
81 state.hits += 1;
82 res.status = 204;
83 res.status_text = "No Content";
84 res.body = "ignored";
85 }
86
87 fn testWsUpgradeHandler(state: *TestState, _: *Request, res: *Response) anyerror!void {
88 state.hits += 1;
89 res.status = 200;
90 res.status_text = "OK";
91 }
92
93 fn testWsHandler(state: *TestState, _: *WebSocket) anyerror!void {
94 state.two = true;
95 }
96
97 fn testReceivingWsHandler(state: *TestState, socket: *WebSocket) anyerror!void {
98 while (try socket.receive()) |received| {
99 state.two = std.mem.eql(u8, received.payload, "hi");
100 }
101 }
102
103 const TestConnectionPair = struct {
104 storage: http.ConnectionInputStorage,
105 request_storage: http.RequestStorage,
106 response_storage: http.ResponseStorage,
107 conn: Connection,
108 peer: sys.Socket,
109
110 fn deinit(self: *TestConnectionPair, allocator: std.mem.Allocator) void {
111 self.conn.deinit();
112 self.response_storage.deinit(allocator);
113 self.request_storage.deinit(allocator);
114 self.storage.deinit(allocator);
115 }
116 };
117
118 fn testConnectionWithInputBytes(
119 allocator: std.mem.Allocator,
120 input_bytes: usize,
121 ) !TestConnectionPair {
122 const sockets = try sys.socketPairUnixStream();
123 errdefer sys.close(sockets[0]);
124 errdefer sys.close(sockets[1]);
125 try sys.setNonBlocking(sockets[0]);
126 var storage = try http.ConnectionInputStorage.init(allocator, .{
127 .connection_count = 1,
128 .bytes_per_connection = input_bytes,
129 });
130 errdefer storage.deinit(allocator);
131 storage.activate();
132 var request_storage = try http.RequestStorage.init(allocator, .{
133 .request_count = 1,
134 .header_count_per_request = http.default_request_header_count,
135 .header_line_bytes = http.default_request_header_line_bytes,
136 .body_bytes_per_request = http.default_request_body_bytes,
137 });
138 errdefer request_storage.deinit(allocator);
139 request_storage.activate();
140 var response_storage = try http.ResponseStorage.init(allocator, .{
141 .response_count = 1,
142 .header_count_per_response = http.default_response_header_count,
143 .head_bytes_per_response = http.default_response_head_bytes,
144 });
145 errdefer response_storage.deinit(allocator);
146 response_storage.activate();
147 return .{
148 .storage = storage,
149 .request_storage = request_storage,
150 .response_storage = response_storage,
151 .conn = Connection.init(
152 1,
153 sockets[0],
154 try storage.connection(0),
155 try request_storage.request(0),
156 try response_storage.response(0),
157 .system(),
158 try time.bootNow(),
159 ),
160 .peer = sockets[1],
161 };
162 }
163
164 fn testConnection(allocator: std.mem.Allocator) !TestConnectionPair {
165 return testConnectionWithInputBytes(
166 allocator,
167 http.default_connection_input_bytes_per_connection,
168 );
169 }
170
171 fn readPeerThrough(peer: sys.Socket, expected: []const u8) ![]u8 {
172 var buffer: [512]u8 = undefined;
173 var length: usize = 0;
174 while (length < buffer.len) {
175 if (std.mem.indexOf(u8, buffer[0..length], expected) != null) {
176 return std.testing.allocator.dupe(u8, buffer[0..length]);
177 }
178 if (!try sys.pollReadable(peer, 2000)) return error.ResponseTimeout;
179 const read = try sys.recv(peer, buffer[length..], 0);
180 if (read == 0) break;
181 length += read;
182 }
183 try std.testing.expect(std.mem.indexOf(u8, buffer[0..length], expected) != null);
184 return std.testing.allocator.dupe(u8, buffer[0..length]);
185 }
186
187 test "Router.handleRequest preserves pipelined request bytes" {
188 comptime {
189 @stardustClaim(
190 @import("alloc_phase").capacity.witness(@import("../root.zig").ConnectionInputStorage, "http_connection_input_pipeline"),
191 null,
192 null,
193 null,
194 null,
195 null,
196 null,
197 );
198 }
199 comptime {
200 @stardustClaim(
201 @import("alloc_phase").capacity.witness(@import("../root.zig").RequestStorage, "http_request_pipeline"),
202 null,
203 null,
204 null,
205 null,
206 null,
207 null,
208 );
209 }
210 comptime {
211 @stardustClaim(
212 @import("alloc_phase").capacity.witness(@import("../root.zig").ResponseStorage, "http_response_router"),
213 null,
214 null,
215 null,
216 null,
217 null,
218 null,
219 );
220 }
221
222 var state = TestState{};
223 var router = Router(*TestState).init(std.testing.allocator, &state);
224 defer router.deinit();
225 try router.get("/one", testHandler);
226 try router.get("/two", testHandler);
227
228 var pair = try testConnection(std.testing.allocator);
229 defer sys.close(pair.peer);
230 defer pair.deinit(std.testing.allocator);
231
232 const requests =
233 "GET /one HTTP/1.1\r\nHost: example.test\r\n\r\n" ++
234 "GET /two HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n";
235 _ = try sys.send(pair.peer, requests, 0);
236
237 try std.testing.expect(try router.handleRequest(&pair.conn));
238 const first = try readPeerThrough(pair.peer, "\r\n\r\n/one");
239 defer std.testing.allocator.free(first);
240 try std.testing.expect(std.mem.indexOf(u8, first, "\r\n\r\n/one") != null);
241
242 try std.testing.expect(!try router.handleRequest(&pair.conn));
243 const second = try readPeerThrough(pair.peer, "\r\n\r\n/two");
244 defer std.testing.allocator.free(second);
245 try std.testing.expect(std.mem.indexOf(u8, second, "\r\n\r\n/two") != null);
246
247 try std.testing.expect(state.one);
248 try std.testing.expect(state.two);
249 try std.testing.expectEqual(@as(usize, 2), state.hits);
250 }
251
252 test "Router.handleRequest preserves partial request across readiness turns" {
253 comptime {
254 @stardustClaim(
255 @import("alloc_phase").capacity.witness(@import("../root.zig").ConnectionInputStorage, "http_connection_input_partial"),
256 null,
257 null,
258 null,
259 null,
260 null,
261 null,
262 );
263 }
264
265 var state = TestState{};
266 var router = Router(*TestState).init(std.testing.allocator, &state);
267 defer router.deinit();
268 try router.get("/one", testHandler);
269
270 var pair = try testConnection(std.testing.allocator);
271 defer sys.close(pair.peer);
272 defer pair.deinit(std.testing.allocator);
273
274 _ = try sys.send(pair.peer, "GET /one HTTP/1.1\r\nHost: exam", 0);
275 try std.testing.expect(!try router.handleRequest(&pair.conn));
276 try std.testing.expectEqual(@as(usize, 0), state.hits);
277
278 _ = try sys.send(pair.peer, "ple.test\r\nConnection: close\r\n\r\n", 0);
279 try std.testing.expect(!try router.handleRequest(&pair.conn));
280 const response = try readPeerThrough(pair.peer, "\r\n\r\n/one");
281 defer std.testing.allocator.free(response);
282 try std.testing.expect(std.mem.indexOf(u8, response, "\r\n\r\n/one") != null);
283 try std.testing.expectEqual(@as(usize, 1), state.hits);
284 }
285
286 test "Router input max plus one leaves socket bytes unread" {
287 comptime {
288 @stardustClaim(
289 @import("alloc_phase").capacity.witness(@import("../root.zig").ConnectionInputStorage, "http_connection_input_overload"),
290 null,
291 null,
292 null,
293 null,
294 null,
295 null,
296 );
297 }
298
299 var state = TestState{};
300 var router = Router(*TestState).init(std.testing.allocator, &state);
301 defer router.deinit();
302 try router.get("/one", testHandler);
303
304 var pair = try testConnectionWithInputBytes(std.testing.allocator, 8);
305 defer sys.close(pair.peer);
306 defer pair.deinit(std.testing.allocator);
307
308 try std.testing.expectEqual(@as(usize, 9), try sys.send(pair.peer, "GET /oneX", 0));
309 try std.testing.expect(!try router.handleRequest(&pair.conn));
310 try std.testing.expectEqualStrings("GET /one", pair.conn.bufferedInput());
311 try std.testing.expect(try sys.pollReadable(pair.conn.socket, 0));
312 try std.testing.expectEqual(
313 http.ConnectionInputStatus{ .capacity_rejections = 1 },
314 pair.conn.inputStatus(),
315 );
316 try std.testing.expectEqual(Connection.State.closing, pair.conn.currentState());
317 try std.testing.expectEqual(@as(usize, 0), state.hits);
318
319 const response = try readPeerThrough(pair.peer, "\r\n\r\nBad Request");
320 defer std.testing.allocator.free(response);
321 try std.testing.expect(std.mem.startsWith(u8, response, "HTTP/1.1 400 Bad Request\r\n"));
322 }
323
324 test "Router.handleRequest tokenizes Connection close for HTTP/1.1" {
325 var state = TestState{};
326 var router = Router(*TestState).init(std.testing.allocator, &state);
327 defer router.deinit();
328 try router.get("/one", testHandler);
329
330 var pair = try testConnection(std.testing.allocator);
331 defer sys.close(pair.peer);
332 defer pair.deinit(std.testing.allocator);
333
334 _ = try sys.send(pair.peer, "GET /one HTTP/1.1\r\nConnection: keep-alive, close\r\n\r\n", 0);
335 try std.testing.expect(!try router.handleRequest(&pair.conn));
336 const response = try readPeerThrough(pair.peer, "\r\n\r\n/one");
337 defer std.testing.allocator.free(response);
338 try std.testing.expect(std.mem.indexOf(u8, response, "\r\n\r\n/one") != null);
339 }
340
341 test "Router.handleRequest tokenizes Connection keep-alive for HTTP/1.0" {
342 var state = TestState{};
343 var router = Router(*TestState).init(std.testing.allocator, &state);
344 defer router.deinit();
345 try router.get("/one", testHandler);
346
347 var pair = try testConnection(std.testing.allocator);
348 defer sys.close(pair.peer);
349 defer pair.deinit(std.testing.allocator);
350
351 _ = try sys.send(pair.peer, "GET /one HTTP/1.0\r\nConnection: keep-alive, upgrade\r\n\r\n", 0);
352 try std.testing.expect(try router.handleRequest(&pair.conn));
353 const response = try readPeerThrough(pair.peer, "\r\n\r\n/one");
354 defer std.testing.allocator.free(response);
355 try std.testing.expect(std.mem.indexOf(u8, response, "\r\n\r\n/one") != null);
356 }
357
358 test "Router.handleRequest omits body for HEAD response" {
359 var state = TestState{};
360 var router = Router(*TestState).init(std.testing.allocator, &state);
361 defer router.deinit();
362 try router.head("/one", testHeadHandler);
363
364 var pair = try testConnection(std.testing.allocator);
365 defer sys.close(pair.peer);
366 defer pair.deinit(std.testing.allocator);
367
368 _ = try sys.send(pair.peer, "HEAD /one HTTP/1.1\r\nConnection: close\r\n\r\n", 0);
369 try std.testing.expect(!try router.handleRequest(&pair.conn));
370 const response = try readPeerThrough(pair.peer, "\r\n\r\n");
371 defer std.testing.allocator.free(response);
372 try std.testing.expect(std.mem.indexOf(u8, response, "Content-Length: 9\r\n") != null);
373 try std.testing.expect(std.mem.endsWith(u8, response, "\r\n\r\n"));
374 try std.testing.expect(std.mem.indexOf(u8, response, "head body") == null);
375 try std.testing.expectEqual(@as(usize, 1), state.hits);
376 }
377
378 test "Router.handleRequest omits body for no-content response" {
379 var state = TestState{};
380 var router = Router(*TestState).init(std.testing.allocator, &state);
381 defer router.deinit();
382 try router.get("/one", testNoContentHandler);
383
384 var pair = try testConnection(std.testing.allocator);
385 defer sys.close(pair.peer);
386 defer pair.deinit(std.testing.allocator);
387
388 _ = try sys.send(pair.peer, "GET /one HTTP/1.1\r\nConnection: close\r\n\r\n", 0);
389 try std.testing.expect(!try router.handleRequest(&pair.conn));
390 const response = try readPeerThrough(pair.peer, "\r\n\r\n");
391 defer std.testing.allocator.free(response);
392 try std.testing.expect(std.mem.startsWith(u8, response, "HTTP/1.1 204 No Content\r\n"));
393 try std.testing.expect(std.mem.indexOf(u8, response, "Content-Length") == null);
394 try std.testing.expect(std.mem.endsWith(u8, response, "\r\n\r\n"));
395 try std.testing.expect(std.mem.indexOf(u8, response, "ignored") == null);
396 try std.testing.expectEqual(@as(usize, 1), state.hits);
397 }
398
399 test "Router.handleRequest completes valid websocket handshake" {
400 var state = TestState{};
401 var router = Router(*TestState).init(std.testing.allocator, &state);
402 defer router.deinit();
403 try router.websocket("/ws", websocket_limits, testWsUpgradeHandler, testWsHandler);
404
405 var pair = try testConnection(std.testing.allocator);
406 defer sys.close(pair.peer);
407 defer pair.deinit(std.testing.allocator);
408
409 const request =
410 "GET /ws HTTP/1.1\r\n" ++
411 "Host: example.test\r\n" ++
412 "Upgrade: websocket\r\n" ++
413 "Connection: keep-alive, Upgrade\r\n" ++
414 "Sec-WebSocket-Version: 13\r\n" ++
415 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
416 "\r\n";
417 _ = try sys.send(pair.peer, request, 0);
418
419 try std.testing.expect(!try router.handleRequest(&pair.conn));
420 const response = try readPeerThrough(pair.peer, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n");
421 defer std.testing.allocator.free(response);
422 try std.testing.expect(std.mem.indexOf(u8, response, "101 Switching Protocols\r\n") != null);
423 try std.testing.expect(std.mem.indexOf(u8, response, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n") != null);
424 try std.testing.expectEqual(@as(usize, 1), state.hits);
425 try std.testing.expectEqual(Connection.State.websocket, pair.conn.currentState());
426 try std.testing.expect(!try router.handleRequest(&pair.conn));
427 try std.testing.expect(state.two);
428 }
429
430 test "Router.handleRequest preserves partial WebSocket frames across readiness turns" {
431 var state = TestState{};
432 var router = Router(*TestState).init(std.testing.allocator, &state);
433 defer router.deinit();
434 try router.websocket("/ws", websocket_limits, testWsUpgradeHandler, testReceivingWsHandler);
435
436 var pair = try testConnection(std.testing.allocator);
437 defer sys.close(pair.peer);
438 defer pair.deinit(std.testing.allocator);
439
440 const request =
441 "GET /ws HTTP/1.1\r\n" ++
442 "Host: example.test\r\n" ++
443 "Upgrade: websocket\r\n" ++
444 "Connection: Upgrade\r\n" ++
445 "Sec-WebSocket-Version: 13\r\n" ++
446 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n";
447 _ = try sys.send(pair.peer, request ++ "\x81\x82\x01\x02\x03\x04", 0);
448 try std.testing.expect(!try router.handleRequest(&pair.conn));
449 const response = try readPeerThrough(pair.peer, "\r\n\r\n");
450 defer std.testing.allocator.free(response);
451
452 const frame = [_]u8{ 0x81, 0x82, 1, 2, 3, 4, 'h' ^ 1, 'i' ^ 2 };
453 try std.testing.expect(!try router.handleRequest(&pair.conn));
454 try std.testing.expect(!state.two);
455 try std.testing.expectEqual(Connection.State.websocket, pair.conn.currentState());
456
457 _ = try sys.send(pair.peer, frame[6..], 0);
458 try std.testing.expect(!try router.handleRequest(&pair.conn));
459 try std.testing.expect(state.two);
460 try std.testing.expectEqual(Connection.State.websocket, pair.conn.currentState());
461 }
462
463 test "upgradeWebsocket acquires bounded storage before the switching response" {
464 comptime {
465 @stardustClaim(
466 @import("alloc_phase").capacity.witness(@import("../root.zig").WebsocketStorage, "http_websocket_upgrade"),
467 null,
468 null,
469 null,
470 null,
471 null,
472 null,
473 );
474 }
475
476 const request_bytes =
477 "GET /ws HTTP/1.1\r\n" ++
478 "Host: example.test\r\n" ++
479 "Upgrade: websocket\r\n" ++
480 "Connection: Upgrade\r\n" ++
481 "Sec-WebSocket-Version: 13\r\n" ++
482 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n";
483 var pair = try testConnection(std.testing.allocator);
484 defer pair.deinit(std.testing.allocator);
485 defer sys.close(pair.peer);
486 const parsed = try Request.parse(pair.conn.requestScratch(), request_bytes);
487 var request = parsed.request;
488 var failing = std.testing.FailingAllocator.init(
489 std.testing.allocator,
490 .{ .fail_index = 0 },
491 );
492
493 try std.testing.expectError(
494 error.OutOfMemory,
495 upgradeWebsocket(
496 failing.allocator(),
497 websocket_limits,
498 &pair.conn,
499 &request,
500 ),
501 );
502 try std.testing.expect(failing.has_induced_failure);
503 try std.testing.expect(!try sys.pollReadable(pair.peer, 0));
504 try std.testing.expectEqual(Connection.State.http, pair.conn.currentState());
505 }
506
507 test "Router.handleRequest rejects websocket handshake with invalid key" {
508 var state = TestState{};
509 var router = Router(*TestState).init(std.testing.allocator, &state);
510 defer router.deinit();
511 try router.websocket("/ws", websocket_limits, testWsUpgradeHandler, testWsHandler);
512
513 var pair = try testConnection(std.testing.allocator);
514 defer sys.close(pair.peer);
515 defer pair.deinit(std.testing.allocator);
516
517 const request =
518 "GET /ws HTTP/1.1\r\n" ++
519 "Host: example.test\r\n" ++
520 "Upgrade: websocket\r\n" ++
521 "Connection: upgrade\r\n" ++
522 "Sec-WebSocket-Version: 13\r\n" ++
523 "Sec-WebSocket-Key: AAAA\r\n" ++
524 "\r\n";
525 _ = try sys.send(pair.peer, request, 0);
526
527 try std.testing.expect(!try router.handleRequest(&pair.conn));
528 const response = try readPeerThrough(pair.peer, "\r\n\r\nBad Request");
529 defer std.testing.allocator.free(response);
530 try std.testing.expect(std.mem.indexOf(u8, response, "400 Bad Request\r\n") != null);
531 try std.testing.expectEqual(@as(usize, 0), state.hits);
532 try std.testing.expect(!state.two);
533 }