Skip to content

Commit f026400

Browse files
authored
Merge pull request #14 from bradcypert/bradcypert/not-equals
Add support for NotEquals assertions
2 parents c47c736 + 325d314 commit f026400

File tree

5 files changed

+200
-153
lines changed

5 files changed

+200
-153
lines changed

build.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,41 @@ pub fn build(b: *std.Build) void {
4444

4545
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
4646

47+
// Automatically find all Zig files in src/ and subdirectories and add test steps for each
48+
var zig_files = std.ArrayList([]const u8).init(b.allocator);
49+
defer zig_files.deinit();
50+
findZigFiles(b, &zig_files, "src") catch unreachable;
51+
52+
var test_run_steps = std.ArrayList(*std.Build.Step).init(b.allocator);
53+
defer test_run_steps.deinit();
54+
55+
for (zig_files.items) |zig_file| {
56+
// Skip main.zig since it's already covered by exe_unit_tests
57+
if (std.mem.endsWith(u8, zig_file, "/main.zig")) continue;
58+
const test_artifact = b.addTest(.{ .root_source_file = b.path(zig_file), .target = target, .optimize = optimize });
59+
const run_test = b.addRunArtifact(test_artifact);
60+
test_run_steps.append(&run_test.step) catch unreachable;
61+
}
62+
4763
const test_step = b.step("test", "Run unit tests");
4864
test_step.dependOn(&run_exe_unit_tests.step);
65+
for (test_run_steps.items) |step| {
66+
test_step.dependOn(step);
67+
}
68+
}
69+
70+
fn findZigFiles(b: *std.Build, files: *std.ArrayList([]const u8), dir_path: []const u8) !void {
71+
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
72+
defer dir.close();
73+
var it = dir.iterate();
74+
while (try it.next()) |entry| {
75+
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".zig")) {
76+
const full_path = try std.fs.path.join(b.allocator, &[_][]const u8{ dir_path, entry.name });
77+
try files.append(full_path);
78+
} else if (entry.kind == .directory and !std.mem.eql(u8, entry.name, ".") and !std.mem.eql(u8, entry.name, "..")) {
79+
const subdir = try std.fs.path.join(b.allocator, &[_][]const u8{ dir_path, entry.name });
80+
defer b.allocator.free(subdir);
81+
try findZigFiles(b, files, subdir);
82+
}
83+
}
4984
}

src/httpfile/assertion_checker.zig

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
2222
} else if (std.mem.startsWith(u8, assertion.key, "header[\"")) {
2323
// Extract the header name from the assertion key
2424
const header_name = assertion.key[8 .. assertion.key.len - 2];
25-
2625
const actual_value = response.headers.get(header_name);
2726
if (actual_value == null or !std.ascii.eqlIgnoreCase(actual_value.?, assertion.value)) {
2827
stderr.print("[Fail] Expected header \"{s}\" to be \"{s}\", got \"{s}\"\n", .{ header_name, assertion.value, actual_value orelse "null" }) catch return error.HeaderMismatch;
@@ -33,6 +32,32 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
3332
return error.InvalidAssertionKey;
3433
}
3534
},
35+
.not_equal => {
36+
if (std.ascii.eqlIgnoreCase(assertion.key, "status")) {
37+
const assert_status_code = try std.fmt.parseInt(u16, assertion.value, 10);
38+
if (response.status == try std.meta.intToEnum(http.Status, assert_status_code)) {
39+
stderr.print("[Fail] Expected status code to NOT equal {d}, got {d}\n", .{ assert_status_code, @intFromEnum(response.status.?) }) catch return error.StatusCodesMatchButShouldnt;
40+
return error.StatusCodesMatchButShouldnt;
41+
}
42+
} else if (std.ascii.eqlIgnoreCase(assertion.key, "body")) {
43+
if (std.mem.eql(u8, response.body, assertion.value)) {
44+
stderr.print("[Fail] Expected body content to NOT equal \"{s}\", got \"{s}\"\n", .{ assertion.value, response.body }) catch return error.BodyContentMatchesButShouldnt;
45+
return error.BodyContentMatchesButShouldnt;
46+
}
47+
} else if (std.mem.startsWith(u8, assertion.key, "header[\"")) {
48+
// Extract the header name from the assertion key
49+
const header_name = assertion.key[8 .. assertion.key.len - 2];
50+
const actual_value = response.headers.get(header_name);
51+
if (actual_value != null and std.ascii.eqlIgnoreCase(actual_value.?, assertion.value)) {
52+
stderr.print("[Fail] Expected header \"{s}\" to NOT equal \"{s}\", got \"{s}\"\n", .{ header_name, assertion.value, actual_value orelse "null" }) catch return error.HeaderMatchesButShouldnt;
53+
return error.HeaderMatchesButShouldnt;
54+
}
55+
} else {
56+
stderr.print("[Fail] Invalid assertion key: {s}\n", .{assertion.key}) catch return error.InvalidAssertionKey;
57+
return error.InvalidAssertionKey;
58+
}
59+
},
60+
3661
// .header => {
3762
// // assertion.key is header[""] so we need to
3863
// // parse it out of the quotes
@@ -101,7 +126,56 @@ test "HttpParser parses assertions" {
101126
.assertion_type = .equal,
102127
});
103128

104-
const request = HttpParser.HttpRequest{
129+
var request = HttpParser.HttpRequest{
130+
.method = .GET,
131+
.url = "https://api.example.com",
132+
.headers = std.ArrayList(http.Header).init(allocator),
133+
.assertions = assertions,
134+
.body = null,
135+
};
136+
137+
var response_headers = std.StringHashMap([]const u8).init(allocator);
138+
try response_headers.put("content-type", "application/json");
139+
defer response_headers.deinit();
140+
141+
const body = try allocator.dupe(u8, "Response body content");
142+
defer allocator.free(body);
143+
const response = Client.HttpResponse{
144+
.status = http.Status.ok,
145+
.headers = response_headers,
146+
.body = body,
147+
.allocator = allocator,
148+
};
149+
150+
try check(&request, response);
151+
}
152+
153+
test "HttpParser handles NotEquals" {
154+
const allocator = std.testing.allocator;
155+
156+
var assertions = std.ArrayList(HttpParser.Assertion).init(allocator);
157+
defer assertions.deinit();
158+
159+
try assertions.append(HttpParser.Assertion{
160+
.key = "status",
161+
.value = "400",
162+
.assertion_type = .not_equal,
163+
});
164+
165+
try assertions.append(HttpParser.Assertion{
166+
.key = "body",
167+
.value = "Response body content!!!",
168+
.assertion_type = .not_equal,
169+
});
170+
171+
// TODO: This should also work with header[\"Content-Type\"] as the key
172+
try assertions.append(HttpParser.Assertion{
173+
.key = "header[\"content-type\"]",
174+
.value = "application/xml",
175+
.assertion_type = .not_equal,
176+
});
177+
178+
var request = HttpParser.HttpRequest{
105179
.method = .GET,
106180
.url = "https://api.example.com",
107181
.headers = std.ArrayList(http.Header).init(allocator),
@@ -122,5 +196,5 @@ test "HttpParser parses assertions" {
122196
.allocator = allocator,
123197
};
124198

125-
try check(request, response);
199+
try check(&request, response);
126200
}

src/httpfile/http_client.zig

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
55
const Uri = std.Uri;
66
const httpfiles = @import("./parser.zig");
77

8+
/// Represents an HTTP response, including status, headers, and body.
89
pub const HttpResponse = struct {
910
status: ?http.Status,
1011
headers: std.StringHashMap([]const u8),
@@ -33,6 +34,7 @@ pub const HttpResponse = struct {
3334
}
3435
};
3536

37+
/// HTTP client for executing requests defined in httpfiles.HttpRequest.
3638
pub const HttpClient = struct {
3739
allocator: Allocator,
3840
client: http.Client,
@@ -48,69 +50,55 @@ pub const HttpClient = struct {
4850
self.client.deinit();
4951
}
5052

53+
/// Executes a single HTTP request and returns the response.
5154
pub fn execute(self: *HttpClient, request: *const httpfiles.HttpRequest) !HttpResponse {
5255
if (request.method == null) {
5356
return error.MissingHttpMethod;
5457
}
5558

56-
// Parse the URL
5759
const uri = try Uri.parse(request.url);
58-
5960
var server_header_buf: [4096]u8 = undefined;
6061

61-
// Create the HTTP request
6262
var req = try self.client.open(request.method.?, uri, .{
6363
.server_header_buffer = &server_header_buf,
6464
.extra_headers = request.headers.items,
6565
});
6666
defer req.deinit();
6767

68-
// Set content length if we have a body
6968
if (request.body) |body| {
7069
req.transfer_encoding = .{ .content_length = body.len };
71-
} else {
72-
//req.transfer_encoding = .{ .content_length = 0 };
7370
}
7471

75-
// Send the request
7672
try req.send();
7773

78-
// Send body if present
7974
if (request.body) |body| {
8075
try req.writeAll(body);
8176
}
8277

8378
try req.finish();
84-
85-
// Wait for response
8679
try req.wait();
8780

88-
// Read the response
8981
var response = HttpResponse.init(self.allocator);
9082
response.status = req.response.status;
9183

92-
// Copy response headers
9384
var header_iterator = req.response.iterateHeaders();
9485
while (header_iterator.next()) |header| {
9586
const name = try self.allocator.dupe(u8, header.name);
9687
const value = try self.allocator.dupe(u8, header.value);
9788
try response.headers.put(name, value);
9889
}
9990

100-
// Read response body
10191
const body_reader = req.reader();
102-
const body_content = try body_reader.readAllAlloc(self.allocator, std.math.maxInt(usize));
103-
response.body = body_content;
92+
response.body = try body_reader.readAllAlloc(self.allocator, std.math.maxInt(usize));
10493

10594
return response;
10695
}
10796

97+
/// Executes multiple HTTP requests and returns an array of responses.
10898
pub fn executeRequests(self: *HttpClient, requests: []const httpfiles.HttpRequest) !ArrayList(HttpResponse) {
10999
var responses = ArrayList(HttpResponse).init(self.allocator);
110100
errdefer {
111-
for (responses.items) |*response| {
112-
response.deinit();
113-
}
101+
for (responses.items) |*response| response.deinit();
114102
responses.deinit();
115103
}
116104

@@ -123,7 +111,7 @@ pub const HttpClient = struct {
123111
}
124112
};
125113

126-
// Utility function to print response details
114+
// Utility function for backwards compatibility.
127115
pub fn printResponse(response: *const HttpResponse) void {
128116
std.debug.print("Status: {d}\n", .{response.status});
129117
std.debug.print("Headers:\n");
@@ -140,7 +128,6 @@ test "HttpClient basic functionality" {
140128
var client = HttpClient.init(allocator);
141129
defer client.deinit();
142130

143-
// Create a simple GET request
144131
var request = httpfiles.HttpRequest.init(allocator);
145132
defer request.deinit(allocator);
146133

@@ -151,10 +138,8 @@ test "HttpClient basic functionality" {
151138
// isn't really intended either. Either way, need to look into this further.
152139
request.url = try allocator.dupe(u8, "https://httpbin.org/status/200");
153140

154-
// Execute the request
155141
var response = try client.execute(&request);
156142
defer response.deinit();
157143

158-
// Check that we got a 200 status
159144
try std.testing.expectEqual(http.Status.ok, response.status.?);
160145
}

0 commit comments

Comments
 (0)