@@ -43,6 +43,7 @@ test "HttpParser supports contains and not_contains for headers" {
4343}
4444const std = @import ("std" );
4545const http = std .http ;
46+ const regex = @import ("regex" );
4647const HttpParser = @import ("./parser.zig" );
4748const Client = @import ("./http_client.zig" );
4849
@@ -54,6 +55,17 @@ fn extractHeaderName(key: []const u8) ![]const u8 {
5455 return key [start_quote + 1 .. end_quote ];
5556}
5657
58+ fn matchesRegex (text : []const u8 , pattern : []const u8 ) bool {
59+ var arena = std .heap .ArenaAllocator .init (std .heap .page_allocator );
60+ defer arena .deinit ();
61+ const allocator = arena .allocator ();
62+
63+ const compiled_regex = regex .compile (allocator , pattern ) catch return false ;
64+ defer compiled_regex .deinit ();
65+
66+ return compiled_regex .match (text );
67+ }
68+
5769pub fn check (request : * HttpParser.HttpRequest , response : Client.HttpResponse ) ! void {
5870 const stderr = std .io .getStdErr ().writer ();
5971 for (request .assertions .items ) | assertion | {
@@ -184,6 +196,58 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
184196 return error .InvalidAssertionKey ;
185197 }
186198 },
199+ .matches_regex = > {
200+ if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
201+ var status_buf : [3 ]u8 = undefined ;
202+ const status_code = @intFromEnum (response .status .? );
203+ const status_str = std .fmt .bufPrint (& status_buf , "{}" , .{status_code }) catch return error .StatusCodeFormat ;
204+ if (! matchesRegex (status_str , assertion .value )) {
205+ stderr .print ("[Fail] Expected status code to match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , status_str }) catch {};
206+ return error .StatusCodeNotMatchesRegex ;
207+ }
208+ } else if (std .ascii .eqlIgnoreCase (assertion .key , "body" )) {
209+ if (! matchesRegex (response .body , assertion .value )) {
210+ stderr .print ("[Fail] Expected body content to match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , response .body }) catch {};
211+ return error .BodyContentNotMatchesRegex ;
212+ }
213+ } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
214+ const header_name = try extractHeaderName (assertion .key );
215+ const actual_value = response .headers .get (header_name );
216+ if (actual_value == null or ! matchesRegex (actual_value .? , assertion .value )) {
217+ stderr .print ("[Fail] Expected header \" {s}\" to match regex \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
218+ return error .HeaderNotMatchesRegex ;
219+ }
220+ } else {
221+ stderr .print ("[Fail] Invalid assertion key for matches_regex: {s}\n " , .{assertion .key }) catch {};
222+ return error .InvalidAssertionKey ;
223+ }
224+ },
225+ .not_matches_regex = > {
226+ if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
227+ var status_buf : [3 ]u8 = undefined ;
228+ const status_code = @intFromEnum (response .status .? );
229+ const status_str = std .fmt .bufPrint (& status_buf , "{}" , .{status_code }) catch return error .StatusCodeFormat ;
230+ if (matchesRegex (status_str , assertion .value )) {
231+ stderr .print ("[Fail] Expected status code to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , status_str }) catch {};
232+ return error .StatusCodeMatchesRegexButShouldnt ;
233+ }
234+ } else if (std .ascii .eqlIgnoreCase (assertion .key , "body" )) {
235+ if (matchesRegex (response .body , assertion .value )) {
236+ stderr .print ("[Fail] Expected body content to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , response .body }) catch {};
237+ return error .BodyContentMatchesRegexButShouldnt ;
238+ }
239+ } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
240+ const header_name = try extractHeaderName (assertion .key );
241+ const actual_value = response .headers .get (header_name );
242+ if (actual_value != null and matchesRegex (actual_value .? , assertion .value )) {
243+ stderr .print ("[Fail] Expected header \" {s}\" to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
244+ return error .HeaderMatchesRegexButShouldnt ;
245+ }
246+ } else {
247+ stderr .print ("[Fail] Invalid assertion key for not_matches_regex: {s}\n " , .{assertion .key }) catch {};
248+ return error .InvalidAssertionKey ;
249+ }
250+ },
187251 else = > {},
188252 }
189253 }
@@ -340,3 +404,61 @@ test "HttpParser supports starts_with for status, body, and header" {
340404
341405 try check (& request , response );
342406}
407+
408+ test "HttpParser supports matches_regex and not_matches_regex for status, body, and headers" {
409+ const allocator = std .testing .allocator ;
410+
411+ var assertions = std .ArrayList (HttpParser .Assertion ).init (allocator );
412+ defer assertions .deinit ();
413+
414+ // Should pass: status matches regex for 2xx codes
415+ try assertions .append (HttpParser.Assertion {
416+ .key = "status" ,
417+ .value = "^2.*" ,
418+ .assertion_type = .matches_regex ,
419+ });
420+
421+ // Should pass: body matches regex for JSON-like content
422+ try assertions .append (HttpParser.Assertion {
423+ .key = "body" ,
424+ .value = ".*success.*" ,
425+ .assertion_type = .matches_regex ,
426+ });
427+
428+ // Should pass: header matches regex for application/* content types
429+ try assertions .append (HttpParser.Assertion {
430+ .key = "header[\" content-type\" ]" ,
431+ .value = "application/.*" ,
432+ .assertion_type = .matches_regex ,
433+ });
434+
435+ // Should pass: status does not match regex for error codes
436+ try assertions .append (HttpParser.Assertion {
437+ .key = "status" ,
438+ .value = "^[45].*" ,
439+ .assertion_type = .not_matches_regex ,
440+ });
441+
442+ var request = HttpParser.HttpRequest {
443+ .method = .GET ,
444+ .url = "https://api.example.com" ,
445+ .headers = std .ArrayList (http .Header ).init (allocator ),
446+ .assertions = assertions ,
447+ .body = null ,
448+ };
449+
450+ var response_headers = std .StringHashMap ([]const u8 ).init (allocator );
451+ try response_headers .put ("content-type" , "application/json" );
452+ defer response_headers .deinit ();
453+
454+ const body = try allocator .dupe (u8 , "Operation success completed" );
455+ defer allocator .free (body );
456+ const response = Client.HttpResponse {
457+ .status = http .Status .ok ,
458+ .headers = response_headers ,
459+ .body = body ,
460+ .allocator = allocator ,
461+ };
462+
463+ try check (& request , response );
464+ }
0 commit comments