From feb2046549c8b0a6545923943a7f4d48d2921690 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 20 Mar 2025 11:39:50 +0800 Subject: [PATCH] add TLS integration test for sync client --- build.zig | 4 +- build.zig.zon | 5 +- src/browser/browser.zig | 2 +- src/http/client.zig | 175 +++++++++++++++++--- src/main_tests.zig | 2 +- src/main_unit_tests.zig | 211 ++++++++++++++++++++++++ src/{unit_tests.zig => test_runner.zig} | 165 ++++++------------ src/testing.zig | 2 +- tests/test_cert.pem | 29 ++++ tests/test_key.pem | 52 ++++++ 10 files changed, 504 insertions(+), 143 deletions(-) create mode 100644 src/main_unit_tests.zig rename src/{unit_tests.zig => test_runner.zig} (68%) create mode 100644 tests/test_cert.pem create mode 100644 tests/test_key.pem diff --git a/build.zig b/build.zig index ca480681..c26988c6 100644 --- a/build.zig +++ b/build.zig @@ -133,8 +133,8 @@ pub fn build(b: *std.Build) !void { // compile const unit_tests = b.addTest(.{ - .root_source_file = b.path("src/unit_tests.zig"), - .test_runner = .{ .path = b.path("src/unit_tests.zig"), .mode = .simple }, + .root_source_file = b.path("src/main_unit_tests.zig"), + .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple }, .target = target, .optimize = mode, }); diff --git a/build.zig.zon b/build.zig.zon index 7540c493..d2097eb4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,6 +4,9 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - .tls = .{ .url = "https://github.com/ianic/tls.zig/archive/96b923fcdaa6371617154857cef7b8337778cbe2.tar.gz", .hash = "122031f94565d7420a155b6eaec65aaa02acc80e75e6f0947899be2106bc3055b1ec" }, + .tls = .{ + .url = "https://github.com/ianic/tls.zig/archive/21aeaa9dd90f89fb86b0cd597f201a2680236f06.tar.gz", + .hash = "1220e584a5962cfba7c2f8d13151754bf76338c9916fedfd9b7a754501b9d9276c61", + }, }, } diff --git a/src/browser/browser.zig b/src/browser/browser.zig index ed0d9af0..310efd92 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -74,7 +74,7 @@ pub const Browser = struct { .allocator = allocator, .http_client = &app.http_client, .session_pool = SessionPool.init(allocator), - .http_client = try http.Client.init(allocator, 5), + .http_client = try http.Client.init(allocator, 5, null), .page_arena = std.heap.ArenaAllocator.init(allocator), }; } diff --git a/src/http/client.zig b/src/http/client.zig index 0a80142d..dc51aad3 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -1,3 +1,20 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . const std = @import("std"); const builtin = @import("builtin"); @@ -30,8 +47,9 @@ pub const Client = struct { state_pool: StatePool, root_ca: tls.config.CertBundle, - pub fn init(allocator: Allocator, max_concurrent: usize) !Client { - var root_ca = try tls.config.CertBundle.fromSystem(allocator); + // we only allow passing in a root_ca for testing + pub fn init(allocator: Allocator, max_concurrent: usize, root_ca_: ?tls.config.CertBundle) !Client { + var root_ca = root_ca_ orelse try tls.config.CertBundle.fromSystem(allocator); errdefer root_ca.deinit(allocator); const state_pool = try StatePool.init(allocator, max_concurrent); @@ -1420,8 +1438,7 @@ pub const Response = struct { fn processData(self: *Response) !?[]u8 { const data = self._data orelse return null; const result = try self._reader.process(data); - self._done = result - .done; + self._done = result.done; self._data = result.unprocessed; // for the next call return result.data; } @@ -1631,13 +1648,13 @@ test "HttpClient Reader: fuzz" { } test "HttpClient: invalid url" { - var client = try Client.init(testing.allocator, 1); + var client = try testClient(); defer client.deinit(); try testing.expectError(error.UriMissingHost, client.request(.GET, "http:///")); } test "HttpClient: sync connect error" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var req = try client.request(.GET, "HTTP://127.0.0.1:9920"); @@ -1645,7 +1662,7 @@ test "HttpClient: sync connect error" { } test "HttpClient: sync no body" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var req = try client.request(.GET, "http://127.0.0.1:9582/http_client/simple"); @@ -1658,8 +1675,24 @@ test "HttpClient: sync no body" { try testing.expectEqual("0", res.header.get("content-length")); } +test "HttpClient: sync tls no body" { + // https://github.com/ianic/tls.zig/issues/10 + for (0..1) |_| { + var client = try testClient(); + defer client.deinit(); + + var req = try client.request(.GET, "https://127.0.0.1:9581/http_client/simple"); + var res = try req.sendSync(.{}); + + try testing.expectEqual(null, try res.next()); + try testing.expectEqual(200, res.header.status); + try testing.expectEqual(1, res.header.count()); + try testing.expectEqual("0", res.header.get("content-length")); + } +} + test "HttpClient: sync with body" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var req = try client.request(.GET, "http://127.0.0.1:9582/http_client/echo"); @@ -1674,8 +1707,84 @@ test "HttpClient: sync with body" { try testing.expectEqual("Close", res.header.get("_connection")); } +test "HttpClient: sync tls with body" { + var arr: std.ArrayListUnmanaged(u8) = .{}; + defer arr.deinit(testing.allocator); + try arr.ensureTotalCapacity(testing.allocator, 20); + + // https://github.com/ianic/tls.zig/issues/10 + for (0..1) |_| { + defer arr.clearRetainingCapacity(); + var client = try testClient(); + defer client.deinit(); + + var req = try client.request(.GET, "https://127.0.0.1:9581/http_client/body"); + var res = try req.sendSync(.{}); + + while (try res.next()) |data| { + arr.appendSliceAssumeCapacity(data); + } + try testing.expectEqual("1234567890abcdefhijk", arr.items); + try testing.expectEqual(201, res.header.status); + try testing.expectEqual(2, res.header.count()); + try testing.expectEqual("20", res.header.get("content-length")); + try testing.expectEqual("HEaDer", res.header.get("another")); + } +} + +test "HttpClient: sync redirect from TLS to Plaintext" { + var arr: std.ArrayListUnmanaged(u8) = .{}; + defer arr.deinit(testing.allocator); + try arr.ensureTotalCapacity(testing.allocator, 20); + + // https://github.com/ianic/tls.zig/issues/10 + for (0..1) |_| { + defer arr.clearRetainingCapacity(); + var client = try testClient(); + defer client.deinit(); + + var req = try client.request(.GET, "https://127.0.0.1:9581/http_client/redirect/insecure"); + var res = try req.sendSync(.{}); + + while (try res.next()) |data| { + arr.appendSliceAssumeCapacity(data); + } + try testing.expectEqual(201, res.header.status); + try testing.expectEqual(4, res.header.count()); + try testing.expectEqual("close", res.header.get("connection")); + try testing.expectEqual("10", res.header.get("content-length")); + try testing.expectEqual("127.0.0.1", res.header.get("_host")); + try testing.expectEqual("Close", res.header.get("_connection")); + } +} + +test "HttpClient: sync redirect plaintext to TLS" { + var arr: std.ArrayListUnmanaged(u8) = .{}; + defer arr.deinit(testing.allocator); + try arr.ensureTotalCapacity(testing.allocator, 20); + + // https://github.com/ianic/tls.zig/issues/10 + for (0..1) |_| { + defer arr.clearRetainingCapacity(); + var client = try testClient(); + defer client.deinit(); + + var req = try client.request(.GET, "http://127.0.0.1:9582/http_client/redirect/secure"); + var res = try req.sendSync(.{}); + + while (try res.next()) |data| { + arr.appendSliceAssumeCapacity(data); + } + try testing.expectEqual(201, res.header.status); + try testing.expectEqual("1234567890abcdefhijk", arr.items); + try testing.expectEqual(2, res.header.count()); + try testing.expectEqual("20", res.header.get("content-length")); + try testing.expectEqual("HEaDer", res.header.get("another")); + } +} + test "HttpClient: sync GET redirect" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var req = try client.request(.GET, "http://127.0.0.1:9582/http_client/redirect"); @@ -1710,7 +1819,7 @@ test "HttpClient: async connect error" { }; var reset: Thread.ResetEvent = .{}; - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var req = try client.request(.GET, "HTTP://127.0.0.1:9920"); @@ -1720,7 +1829,7 @@ test "HttpClient: async connect error" { } test "HttpClient: async no body" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -1731,8 +1840,7 @@ test "HttpClient: async no body" { var req = try client.request(.GET, "HTTP://127.0.0.1:9582/http_client/simple"); try req.sendAsync(&handler.loop, &handler, .{}); - try handler.loop.io.run_for_ns(std.time.ns_per_ms); - try handler.reset.timedWait(std.time.ns_per_s); + try handler.waitUntilDone(); const res = handler.response; try testing.expectEqual("", res.body.items); @@ -1740,8 +1848,28 @@ test "HttpClient: async no body" { try res.assertHeaders(&.{ "connection", "close", "content-length", "0" }); } +// test "HttpClient: async tls no body" { +// var client = try testClient(); +// defer client.deinit(); + +// var handler = try CaptureHandler.init(); +// defer handler.deinit(); + +// var loop = try jsruntime.Loop.init(testing.allocator); +// defer loop.deinit(); + +// var req = try client.request(.GET, "HTTPs://127.0.0.1:9581/http_client/simple"); +// try req.sendAsync(&handler.loop, &handler, .{}); +// try handler.waitUntilDone(); + +// const res = handler.response; +// try testing.expectEqual("", res.body.items); +// try testing.expectEqual(200, res.status); +// try res.assertHeaders(&.{ "connection", "close", "content-length", "0" }); +// } + test "HttpClient: async with body" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -1749,8 +1877,7 @@ test "HttpClient: async with body" { var req = try client.request(.GET, "HTTP://127.0.0.1:9582/http_client/echo"); try req.sendAsync(&handler.loop, &handler, .{}); - try handler.loop.io.run_for_ns(std.time.ns_per_ms); - try handler.reset.timedWait(std.time.ns_per_s); + try handler.waitUntilDone(); const res = handler.response; try testing.expectEqual("over 9000!", res.body.items); @@ -1764,7 +1891,7 @@ test "HttpClient: async with body" { } test "HttpClient: async redirect" { - var client = try Client.init(testing.allocator, 2); + var client = try testClient(); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -1781,8 +1908,7 @@ test "HttpClient: async redirect" { // start to requeue events (from the redirected request), so we need the //loop to process those also. try handler.loop.io.run_for_ns(std.time.ns_per_ms); - try handler.loop.io.run_for_ns(std.time.ns_per_ms); - try handler.reset.timedWait(std.time.ns_per_s); + try handler.waitUntilDone(); const res = handler.response; try testing.expectEqual("over 9000!", res.body.items); @@ -1885,6 +2011,11 @@ const CaptureHandler = struct { self.reset.set(); } } + + fn waitUntilDone(self: *CaptureHandler) !void { + try self.loop.io.run_for_ns(std.time.ns_per_ms); + try self.reset.timedWait(std.time.ns_per_s); + } }; fn testReader(state: *State, res: *TestResponse, data: []const u8) !void { @@ -1928,3 +2059,9 @@ fn testReader(state: *State, res: *TestResponse, data: []const u8) !void { } return error.NeverDone; } + +fn testClient() !Client { + const test_dir = try std.fs.cwd().openDir("tests", .{}); + const root_ca = try tls.config.CertBundle.fromFile(testing.allocator, test_dir, "test_cert.pem"); + return try Client.init(testing.allocator, 1, root_ca); +} diff --git a/src/main_tests.zig b/src/main_tests.zig index 41560b99..698e1cdd 100644 --- a/src/main_tests.zig +++ b/src/main_tests.zig @@ -88,7 +88,7 @@ fn testExecFn( std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)}); }; - var http_client = try @import("http/client.zig").Client.init(alloc, 5); + var http_client = try @import("http/client.zig").Client.init(alloc, 5, null); defer http_client.deinit(); try js_env.setUserContext(.{ diff --git a/src/main_unit_tests.zig b/src/main_unit_tests.zig new file mode 100644 index 00000000..6f0a19c2 --- /dev/null +++ b/src/main_unit_tests.zig @@ -0,0 +1,211 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const parser = @import("netsurf"); +const tls = @import("tls"); + +const Allocator = std.mem.Allocator; + +test { + std.testing.refAllDecls(@import("url/query.zig")); + std.testing.refAllDecls(@import("browser/dump.zig")); + std.testing.refAllDecls(@import("browser/mime.zig")); + std.testing.refAllDecls(@import("css/css.zig")); + std.testing.refAllDecls(@import("css/libdom_test.zig")); + std.testing.refAllDecls(@import("css/match_test.zig")); + std.testing.refAllDecls(@import("css/parser.zig")); + std.testing.refAllDecls(@import("generate.zig")); + std.testing.refAllDecls(@import("http/client.zig")); + std.testing.refAllDecls(@import("storage/storage.zig")); + std.testing.refAllDecls(@import("storage/cookie.zig")); + std.testing.refAllDecls(@import("iterator/iterator.zig")); + std.testing.refAllDecls(@import("server.zig")); + std.testing.refAllDecls(@import("cdp/cdp.zig")); + std.testing.refAllDecls(@import("log.zig")); + std.testing.refAllDecls(@import("datetime.zig")); + std.testing.refAllDecls(@import("telemetry/telemetry.zig")); + std.testing.refAllDecls(@import("http/client.zig")); +} + +var wg: std.Thread.WaitGroup = .{}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +test "tests:beforeAll" { + try parser.init(); + wg.startMany(3); + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9582); + const thread = try std.Thread.spawn(.{}, serveHTTP, .{ address }); + thread.detach(); + } + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9581); + const thread = try std.Thread.spawn(.{}, serveHTTPS, .{ address }); + thread.detach(); + } + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9583); + const thread = try std.Thread.spawn(.{}, serveCDP, .{address}); + thread.detach(); + } + // need to wait for the servers to be listening, else tests will fail because + // they aren't able to connect. + wg.wait(); +} + +test "tests:afterAll" { + parser.deinit(); +} + +fn serveHTTP(address: std.net.Address) !void { + const allocator = gpa.allocator(); + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var listener = try address.listen(.{ .reuse_address = true }); + defer listener.deinit(); + + wg.finish(); + + var read_buffer: [1024]u8 = undefined; + ACCEPT: while (true) { + defer _ = arena.reset(.{ .retain_with_limit = 1024 }); + const aa = arena.allocator(); + + var conn = try listener.accept(); + defer conn.stream.close(); + var server = std.http.Server.init(conn, &read_buffer); + + while (server.state == .ready) { + var request = server.receiveHead() catch |err| switch (err) { + error.HttpConnectionClosing => continue :ACCEPT, + else => { + std.debug.print("Test HTTP Server error: {}\n", .{err}); + return err; + }, + }; + + const path = request.head.target; + if (std.mem.eql(u8, path, "/loader")) { + try request.respond("Hello!", .{}); + } else if (std.mem.eql(u8, path, "/http_client/simple")) { + try request.respond("", .{}); + } else if (std.mem.eql(u8, path, "/http_client/redirect")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/echo")) { + var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; + + var it = request.iterateHeaders(); + while (it.next()) |hdr| { + try headers.append(aa, .{ + .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), + .value = hdr.value, + }); + } + + try request.respond("over 9000!", .{ + .status = .created, + .extra_headers = headers.items, + }); + } + } + } +} + +// This is a lot of work for testing TLS, but the TLS (async) code is complicated +// This "server" is written specifically to test the client. It assumes the client +// isn't a jerk. +fn serveHTTPS(address: std.net.Address) !void { + const allocator = gpa.allocator(); + const test_dir = try std.fs.cwd().openDir("tests", .{}); + + // openssl req -x509 -newkey rsa:4096 -keyout tests/test_key.pem -out tests/test_cert.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" + var auth = try tls.config.CertKeyPair.load(allocator, test_dir, "test_cert.pem", "test_key.pem"); + defer auth.deinit(allocator); + + var listener = try address.listen(.{ .reuse_address = true }); + defer listener.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + wg.finish(); + + var seed: u64 = undefined; + std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; + var r = std.Random.DefaultPrng.init(seed); + const rand = r.random(); + + var read_buffer: [1024]u8 = undefined; + while (true) { + // defer _ = arena.reset(.{ .retain_with_limit = 1024 }); + // const aa = arena.allocator(); + + const stream = blk: { + const conn = try listener.accept(); + break :blk conn.stream; + }; + defer stream.close(); + + var conn = try tls.server(stream, .{ .auth = &auth }); + defer conn.close() catch {}; + + var pos: usize = 0; + while (true) { + const n = try conn.read(read_buffer[pos..]); + if (n == 0) { + break; + } + pos += n; + const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse { + continue; + }; + var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' '); + _ = it.next() orelse unreachable; // method + const path = it.next() orelse unreachable; + + var response: []const u8 = undefined; + if (std.mem.eql(u8, path, "/http_client/simple")) { + response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n"; + } else if (std.mem.eql(u8, path, "/http_client/body")) { + response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk"; + } else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) { + response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n"; + } else { + // should not have an unknown path + unreachable; + } + + var unsent = response; + while (unsent.len > 0) { + const to_send = rand.intRangeAtMost(usize, 1, unsent.len); + const sent = try conn.write(unsent[0..to_send]); + unsent = unsent[sent..]; + // std.time.sleep(std.time.ns_per_us * 5); + } + break; + } + } +} + +fn serveCDP(address: std.net.Address) !void { + const App = @import("app.zig").App; + var app = try App.init(gpa.allocator(), .serve); + defer app.deinit(); + + const server = @import("server.zig"); + wg.finish(); + server.run(&app, address, std.time.ns_per_s * 2) catch |err| { + std.debug.print("CDP server error: {}", .{err}); + return err; + }; +} diff --git a/src/unit_tests.zig b/src/test_runner.zig similarity index 68% rename from src/unit_tests.zig rename to src/test_runner.zig index f3c12ca1..f43b5735 100644 --- a/src/unit_tests.zig +++ b/src/test_runner.zig @@ -18,34 +18,31 @@ const std = @import("std"); const builtin = @import("builtin"); -const parser = @import("netsurf"); const Allocator = std.mem.Allocator; -const App = @import("app.zig").App; -const jsruntime = @import("jsruntime"); -pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){}); -pub const UserContext = @import("user_context.zig").UserContext; - -pub const std_options = std.Options{ - .log_level = .err, - .http_disable_tls = true, -}; - const BORDER = "=" ** 80; // use in custom panic handler var current_test: ?[]const u8 = null; +const jsruntime = @import("jsruntime"); +pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){}); +pub const UserContext = @import("user_context.zig").UserContext; + +pub const std_options = std.Options{ + .log_level = .warn, + + // Crypto in Zig is generally slow, but it's particularly slow in debug mode + // this helps a lot (but it's still slow). Not safe to do this in non-test! + .side_channels_mitigations = .none, +}; + pub fn main() !void { - try parser.init(); - defer parser.deinit(); + var mem: [8192]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&mem); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - var app = try App.init(allocator, .serve); - defer app.deinit(); + const allocator = fba.allocator(); const env = Env.init(allocator); defer env.deinit(allocator); @@ -58,30 +55,20 @@ pub fn main() !void { var skip: usize = 0; var leak: usize = 0; - const http_thread = blk: { - const address = try std.net.Address.parseIp("127.0.0.1", 9582); - const thread = try std.Thread.spawn(.{}, serveHTTP, .{ allocator, address }); - break :blk thread; - }; - defer http_thread.join(); - - const cdp_thread = blk: { - const address = try std.net.Address.parseIp("127.0.0.1", 9583); - const thread = try std.Thread.spawn(.{}, serveCDP, .{ - app, - address, - }); - break :blk thread; - }; - defer cdp_thread.join(); - const printer = Printer.init(); printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line for (builtin.test_functions) |t| { - if (std.mem.eql(u8, t.name, "unit_tests.test_0")) { - // don't display anything for this test - try t.func(); + if (isSetup(t)) { + t.func() catch |err| { + printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err }); + return err; + }; + } + } + + for (builtin.test_functions) |t| { + if (isSetup(t) or isTeardown(t)) { continue; } @@ -151,6 +138,15 @@ pub fn main() !void { } } + for (builtin.test_functions) |t| { + if (isTeardown(t)) { + t.func() catch |err| { + printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err }); + return err; + }; + } + } + const total_tests = pass + fail; const status = if (fail == 0) Status.pass else Status.fail; printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" }); @@ -315,6 +311,15 @@ const Env = struct { } }; +pub const panic = std.debug.FullPanic(struct { + pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { + if (current_test) |ct| { + std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER }); + } + std.debug.defaultPanic(msg, first_trace_addr); + } +}.panicFn); + fn isUnnamed(t: std.builtin.TestFn) bool { const marker = ".test_"; const test_name = t.name; @@ -323,86 +328,10 @@ fn isUnnamed(t: std.builtin.TestFn) bool { return true; } -fn serveHTTP(allocator: Allocator, address: std.net.Address) !void { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var listener = try address.listen(.{ .reuse_address = true }); - defer listener.deinit(); - - var read_buffer: [1024]u8 = undefined; - ACCEPT: while (true) { - defer _ = arena.reset(.{ .retain_with_limit = 1024 }); - const aa = arena.allocator(); - - var conn = try listener.accept(); - defer conn.stream.close(); - var server = std.http.Server.init(conn, &read_buffer); - - while (server.state == .ready) { - var request = server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => continue :ACCEPT, - else => { - std.debug.print("Test HTTP Server error: {}\n", .{err}); - return err; - }, - }; - - const path = request.head.target; - if (std.mem.eql(u8, path, "/loader")) { - try request.respond("Hello!", .{}); - } else if (std.mem.eql(u8, path, "/http_client/simple")) { - try request.respond("", .{}); - } else if (std.mem.eql(u8, path, "/http_client/redirect")) { - try request.respond("", .{ - .status = .moved_permanently, - .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }}, - }); - } else if (std.mem.eql(u8, path, "/http_client/echo")) { - var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; - - var it = request.iterateHeaders(); - while (it.next()) |hdr| { - try headers.append(aa, .{ - .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), - .value = hdr.value, - }); - } - - try request.respond("over 9000!", .{ - .status = .created, - .extra_headers = headers.items, - }); - } - } - } +fn isSetup(t: std.builtin.TestFn) bool { + return std.mem.endsWith(u8, t.name, "tests:beforeAll"); } -fn serveCDP(app: *App, address: std.net.Address) !void { - const server = @import("server.zig"); - server.run(app, address, std.time.ns_per_s * 2) catch |err| { - std.debug.print("CDP server error: {}", .{err}); - return err; - }; -} - -test { - std.testing.refAllDecls(@import("url/query.zig")); - std.testing.refAllDecls(@import("browser/dump.zig")); - std.testing.refAllDecls(@import("browser/mime.zig")); - std.testing.refAllDecls(@import("css/css.zig")); - std.testing.refAllDecls(@import("css/libdom_test.zig")); - std.testing.refAllDecls(@import("css/match_test.zig")); - std.testing.refAllDecls(@import("css/parser.zig")); - std.testing.refAllDecls(@import("generate.zig")); - std.testing.refAllDecls(@import("http/client.zig")); - std.testing.refAllDecls(@import("storage/storage.zig")); - std.testing.refAllDecls(@import("storage/cookie.zig")); - std.testing.refAllDecls(@import("iterator/iterator.zig")); - std.testing.refAllDecls(@import("server.zig")); - std.testing.refAllDecls(@import("cdp/cdp.zig")); - std.testing.refAllDecls(@import("log.zig")); - std.testing.refAllDecls(@import("datetime.zig")); - std.testing.refAllDecls(@import("telemetry/telemetry.zig")); - std.testing.refAllDecls(@import("http/client.zig")); +fn isTeardown(t: std.builtin.TestFn) bool { + return std.mem.endsWith(u8, t.name, "tests:afterAll"); } diff --git a/src/testing.zig b/src/testing.zig index b1baa731..4efb583d 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -167,7 +167,7 @@ pub const Random = struct { var seed: u64 = undefined; std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; instance = std.Random.DefaultPrng.init(seed); - instance = std.Random.DefaultPrng.init(0); + // instance = std.Random.DefaultPrng.init(0); } return instance.?.random(); } diff --git a/tests/test_cert.pem b/tests/test_cert.pem new file mode 100644 index 00000000..343d0afc --- /dev/null +++ b/tests/test_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUQizPG9ybhujB5BEDeapY//wFwgwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTI1MDMxOTEyMjYzNFoXDTM1MDMx +NzEyMjYzNFowFDESMBAGA1UEAwwJMTI3LjAuMC4xMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAzDHVQ7R3OxOxhoDILfEBkvSGcE6aeI+kEV52qmFzlFM9 +HCT+m4pSaMhtlsqwUA1N5FgkPD9wmka1EqrExnGdFbpZ6uWe0nTUMhNag5pHBT2/ +aYjEWcs/6C+lyve04w12jgkzvQcgbrQZTwVe2cxA2GTnggtiMrwhIuDjKpO12Yy5 +Fn+SrbhLtD7gYM3EOqrP7UukA/BKjYCEBKYkjBXjdODU7+xp8AYUwMQcOgIpMVAR +yJIGbKZ8PfLrmt8EaSHNe0d44YeLgRlOd9raw9aSKxdNDNYWYfAIDtV92gRhH24q +mi+THG8MUeAM7OpssQNNcH0En5ZmOGcKhwaTYM41jNZvPosVfrRTVGQimjrz42IZ +QUx5V+JxY5VMWrwxWxjPallKVN+2LCLdBV6I+zCrwghvQgc4UL8F6hTVg7aIez7E +oNamG77ooHc53eHBmIFGokhuPhFs0HVRIQdFO7Sm1Y1Lk6kek0SlZPge1x0nODRS +tPBpYJlnfzIzu7k8c9u7u7VhUqkRphxKz5xanKslozCjzk5UnmKbtQDJKQOcv+OO +pAm1InDDlw84GKw77aXfQQIJmej0sbDg0NadTMlaOpKnbh7VZBs2VYFmf42B1r1m +4yOdqdnDS/q0m+tkFZbBhDXuSOAkutNT7MUJXdL+/S68k+oHkGXMG/xkYNqTIuUC +AwEAAaNTMFEwHQYDVR0OBBYEFC0RmoMUNOMdBwdVnq2IjDG0B4HLMB8GA1UdIwQY +MBaAFC0RmoMUNOMdBwdVnq2IjDG0B4HLMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAB/2OZOz/V3Qz9RG/XQfPtfDTzV2dzEszaha8CRat5TmEcg+ +s0AcqEyQd2Zn6DOcKeykRx6H6LtP0o+LyNmvRghoWbaz8zknikPotIu7Zw2cKfIq +2gjAq+sZCGkNsMoEVDurVtGmhgtnP1QVMUhW+2gsFXZQhtFjX2jYtMbuJ6A0or7q +Zf1ESCBemTFpgGvLZGiKwc4Re1vrNguyLhln2lYRfSh5UwSyL+KhnB67CpIV6rkc +jYOnN1VffxigqwBBeFWAZG12z8/5cwzmGno9f7w89s4rk1vo3YzZYq/jVs54lkS+ +JlVr8Guws4w1cb7fMRPf+Z28bBcPya4dPFrxbmi/9TK/AEEF2Yc7NgBCa3UI4B8j +2hdLKGq3Tj9Pi2XGIgJ4eejP2F1S2gOq2S10ix/kRyRTIcRaq1TvZt1WzaeLCZEk +gZQH5wS0BY/RvqqQf24APmzsHSFHFW7njpIEXJW4dvnVLJDZvk9r4cJXoTKTXjTb +wyOa4zHkrXW6a3OO+dehFyII6byqOz0oD3LVFF/Cxm3cUKaCBs7QrEDDri3bDu1w +Z/KJsOUV/Rv7YhgonfnAbKfmXAabKeGBSDA1jZG/cXE2qAgW49PUGsjhXLn0ENz4 +nOwCbUGCpnX8m5Myxx5rbJDliG3h+F937o7kyCDPqYaGRjGXbBjP4oaMhaHR +-----END CERTIFICATE----- diff --git a/tests/test_key.pem b/tests/test_key.pem new file mode 100644 index 00000000..6fa8703c --- /dev/null +++ b/tests/test_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Z31M1oPIcKZ4ayKKiG5GYX/+juUlMA== +-----END PRIVATE KEY-----