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 }