From a7a904aea4de5e358c814fca0196b7397fb93138 Mon Sep 17 00:00:00 2001 From: Yanis Fourel Date: Sun, 4 May 2025 18:11:33 +0800 Subject: [PATCH] Add test_recvfile.zig --- build.zig | 16 +++++ src/tests/test_recvfile.zig | 133 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/tests/test_recvfile.zig diff --git a/build.zig b/build.zig index ad14efa..aaef1ab 100644 --- a/build.zig +++ b/build.zig @@ -186,6 +186,17 @@ pub fn build(b: *std.Build) !void { const run_sendfile_tests = b.addRunArtifact(sendfile_tests); const install_sendfile_tests = b.addInstallArtifact(sendfile_tests, .{}); + const recvfile_tests = b.addTest(.{ + .name = "recv_tests", + .root_source_file = b.path("src/tests/test_recvfile.zig"), + .target = target, + .optimize = optimize, + }); + + recvfile_tests.root_module.addImport("zap", zap_module); + const run_recvfile_tests = b.addRunArtifact(recvfile_tests); + const install_recvfile_tests = b.addInstallArtifact(recvfile_tests, .{}); + // test commands const run_auth_test_step = b.step("test-authentication", "Run auth unit tests [REMOVE zig-cache!]"); run_auth_test_step.dependOn(&run_auth_tests.step); @@ -203,6 +214,10 @@ pub fn build(b: *std.Build) !void { run_sendfile_test_step.dependOn(&run_sendfile_tests.step); run_sendfile_test_step.dependOn(&install_sendfile_tests.step); + const run_recvfile_test_step = b.step("test-recvfile", "Run http param unit tests [REMOVE zig-cache!]"); + run_recvfile_test_step.dependOn(&run_recvfile_tests.step); + run_recvfile_test_step.dependOn(&install_recvfile_tests.step); + // Similar to creating the run step earlier, this exposes a `test` step to // the `zig build --help` menu, providing a way for the participant to request // running the unit tests. @@ -211,6 +226,7 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(&run_mustache_tests.step); test_step.dependOn(&run_httpparams_tests.step); test_step.dependOn(&run_sendfile_tests.step); + test_step.dependOn(&run_recvfile_tests.step); // // docserver diff --git a/src/tests/test_recvfile.zig b/src/tests/test_recvfile.zig new file mode 100644 index 0000000..f232020 --- /dev/null +++ b/src/tests/test_recvfile.zig @@ -0,0 +1,133 @@ +const std = @import("std"); +const zap = @import("zap"); + +// set default log level to .info and ZAP log level to .debug +pub const std_options: std.Options = .{ + .log_level = .info, + .log_scope_levels = &[_]std.log.ScopeLevel{ + .{ .scope = .zap, .level = .debug }, + }, +}; + +const BOUNDARY = "---XcPmntPm3EGd9NaxNUPFFL"; +const PARAM_NAME = "file"; + +const EXPECTED_CONTENT = "Hello, this is a test file."; +const EXPECTED_MIMETYPE = "text/plain"; +const EXPECTED_FILENAME = "myfile.txt"; + +var test_error: ?anyerror = null; + +fn makeRequest(allocator: std.mem.Allocator, url: []const u8) !void { + var http_client: std.http.Client = .{ .allocator = allocator }; + defer http_client.deinit(); + + const payload_wrong_line_ending = try std.fmt.allocPrint(allocator, + \\--{s} + \\Content-Disposition: form-data; name="{s}"; filename="{s}" + \\Content-Type: {s} + \\ + \\{s} + \\--{s}-- + \\ + , .{ BOUNDARY, PARAM_NAME, EXPECTED_FILENAME, EXPECTED_MIMETYPE, EXPECTED_CONTENT.*, BOUNDARY }); + defer allocator.free(payload_wrong_line_ending); + + const payload = try std.mem.replaceOwned(u8, allocator, payload_wrong_line_ending, "\n", "\r\n"); + defer allocator.free(payload); + + const request_content_type = try std.fmt.allocPrint( + allocator, + "multipart/form-data; boundary={s}", + .{BOUNDARY}, + ); + defer allocator.free(request_content_type); + + // Allocate a buffer for server headers + var buf: [4096]u8 = undefined; + _ = try http_client.fetch(.{ + .method = .POST, + .location = .{ .url = url }, + .headers = .{ + .content_type = .{ .override = request_content_type }, + }, + .payload = payload, + .server_header_buffer = &buf, + }); + + zap.stop(); +} + +pub fn on_request(r: zap.Request) !void { + on_request_inner(r) catch |err| { + test_error = err; + return; + }; +} + +pub fn on_request_inner(r: zap.Request) !void { + try r.parseBody(); + const params = try r.parametersToOwnedList(std.testing.allocator); + defer params.deinit(); + + std.testing.expect(params.items.len == 1) catch |err| { + std.debug.print("Expected exactly one parameter, got {}\n", .{params.items.len}); + return err; + }; + const param = params.items[0]; + std.testing.expect(param.value != null) catch |err| { + std.debug.print("Expected parameter value to be non-null, got null\n", .{}); + return err; + }; + const value = param.value.?; + + std.testing.expect(value == .Hash_Binfile) catch |err| { + std.debug.print("Expected parameter type to be Hash_Binfile, got {}\n", .{value}); + return err; + }; + const file = value.Hash_Binfile; + std.testing.expect(file.data != null) catch |err| { + std.debug.print("Expected file data to be non-null, got null\n", .{}); + return err; + }; + std.testing.expect(file.mimetype != null) catch |err| { + std.debug.print("Expected file mimetype to be non-null, got null\n", .{}); + return err; + }; + std.testing.expect(file.filename != null) catch |err| { + std.debug.print("Expected file filename to be non-null, got null\n", .{}); + return err; + }; + + // These will print the error if the test fails + try std.testing.expectEqualStrings(file.data.?, &EXPECTED_CONTENT.*); + try std.testing.expectEqualStrings(file.mimetype.?, &EXPECTED_MIMETYPE.*); + try std.testing.expectEqualStrings(file.filename.?, &EXPECTED_FILENAME.*); +} + +test "recv file" { + const allocator = std.testing.allocator; + + var listener = zap.HttpListener.init( + .{ + .port = 3003, + .on_request = on_request, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + try listener.listen(); + + const t1 = try std.Thread.spawn(.{}, makeRequest, .{ allocator, "http://127.0.0.1:3003" }); + defer t1.join(); + + zap.start(.{ + .threads = 1, + .workers = 1, + }); + + if (test_error) |err| { + return err; + } +}