@@ -46,6 +46,14 @@ const http = std.http;
4646const HttpParser = @import ("./parser.zig" );
4747const Client = @import ("./http_client.zig" );
4848
49+ fn extractHeaderName (key : []const u8 ) ! []const u8 {
50+ // Expects key in the form header["..."]
51+ const start_quote = std .mem .indexOfScalar (u8 , key , '"' ) orelse return error .InvalidAssertionKey ;
52+ const end_quote = std .mem .lastIndexOfScalar (u8 , key , '"' ) orelse return error .InvalidAssertionKey ;
53+ if (end_quote <= start_quote ) return error .InvalidAssertionKey ;
54+ return key [start_quote + 1 .. end_quote ];
55+ }
56+
4957pub fn check (request : * HttpParser.HttpRequest , response : Client.HttpResponse ) ! void {
5058 const stderr = std .io .getStdErr ().writer ();
5159 for (request .assertions .items ) | assertion | {
@@ -63,8 +71,7 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
6371 return error .BodyContentMismatch ;
6472 }
6573 } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
66- // Extract the header name from the assertion key
67- const header_name = assertion .key [8 .. assertion .key .len - 2 ];
74+ const header_name = try extractHeaderName (assertion .key );
6875 const actual_value = response .headers .get (header_name );
6976 if (actual_value == null or ! std .ascii .eqlIgnoreCase (actual_value .? , assertion .value )) {
7077 stderr .print ("[Fail] Expected header \" {s}\" to be \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
@@ -88,8 +95,7 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
8895 return error .BodyContentMatchesButShouldnt ;
8996 }
9097 } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
91- // Extract the header name from the assertion key
92- const header_name = assertion .key [8 .. assertion .key .len - 2 ];
98+ const header_name = try extractHeaderName (assertion .key );
9399 const actual_value = response .headers .get (header_name );
94100 if (actual_value != null and std .ascii .eqlIgnoreCase (actual_value .? , assertion .value )) {
95101 stderr .print ("[Fail] Expected header \" {s}\" to NOT equal \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
@@ -100,20 +106,6 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
100106 return error .InvalidAssertionKey ;
101107 }
102108 },
103-
104- // .header => {
105- // // assertion.key is header[""] so we need to
106- // // parse it out of the quotes
107- // const tokens = std.mem.splitScalar(u8, assertion.key, '\"');
108- // const expected_header = tokens.next() orelse return error.InvalidHeaderFormat;
109- // if (expected_header.len != 2) {
110- // return error.InvalidHeaderFormat;
111- // }
112- // const actual_value = response.headers.get(expected_header);
113- // if (actual_value == null or actual_value.* != expected_header.value) {
114- // return error.HeaderMismatch;
115- // }
116- // },
117109 .contains = > {
118110 if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
119111 var status_buf : [3 ]u8 = undefined ;
@@ -129,8 +121,7 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
129121 return error .BodyContentNotContains ;
130122 }
131123 } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
132- // Extract the header name from the assertion key
133- const header_name = assertion .key [8 .. assertion .key .len - 2 ];
124+ const header_name = try extractHeaderName (assertion .key );
134125 const actual_value = response .headers .get (header_name );
135126 if (actual_value == null or std .mem .indexOf (u8 , actual_value .? , assertion .value ) == null ) {
136127 stderr .print ("[Fail] Expected header \" {s}\" to contain \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
@@ -156,8 +147,7 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
156147 return error .BodyContentContainsButShouldnt ;
157148 }
158149 } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
159- // Extract the header name from the assertion key
160- const header_name = assertion .key [8 .. assertion .key .len - 2 ];
150+ const header_name = try extractHeaderName (assertion .key );
161151 const actual_value = response .headers .get (header_name );
162152 if (actual_value != null and std .mem .indexOf (u8 , actual_value .? , assertion .value ) != null ) {
163153 stderr .print ("[Fail] Expected header \" {s}\" to NOT contain \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
@@ -168,6 +158,32 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
168158 return error .InvalidAssertionKey ;
169159 }
170160 },
161+ .starts_with = > {
162+ if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
163+ var status_buf : [3 ]u8 = undefined ;
164+ const status_code = @intFromEnum (response .status .? );
165+ const status_str = std .fmt .bufPrint (& status_buf , "{}" , .{status_code }) catch return error .StatusCodeFormat ;
166+ if (! std .mem .startsWith (u8 , status_str , assertion .value )) {
167+ stderr .print ("[Fail] Expected status code to start with \" {s}\" , got \" {s}\" \n " , .{ assertion .value , status_str }) catch {};
168+ return error .StatusCodeNotStartsWith ;
169+ }
170+ } else if (std .ascii .eqlIgnoreCase (assertion .key , "body" )) {
171+ if (! std .mem .startsWith (u8 , response .body , assertion .value )) {
172+ stderr .print ("[Fail] Expected body content to start with \" {s}\" , got \" {s}\" \n " , .{ assertion .value , response .body }) catch {};
173+ return error .BodyContentNotStartsWith ;
174+ }
175+ } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
176+ const header_name = try extractHeaderName (assertion .key );
177+ const actual_value = response .headers .get (header_name );
178+ if (actual_value == null or ! std .mem .startsWith (u8 , actual_value .? , assertion .value )) {
179+ stderr .print ("[Fail] Expected header \" {s}\" to start with \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
180+ return error .HeaderNotStartsWith ;
181+ }
182+ } else {
183+ stderr .print ("[Fail] Invalid assertion key for starts_with: {s}\n " , .{assertion .key }) catch {};
184+ return error .InvalidAssertionKey ;
185+ }
186+ },
171187 else = > {},
172188 }
173189 }
@@ -276,3 +292,51 @@ test "HttpParser handles NotEquals" {
276292
277293 try check (& request , response );
278294}
295+
296+ test "HttpParser supports starts_with for status, body, and header" {
297+ const allocator = std .testing .allocator ;
298+ var assertions = std .ArrayList (HttpParser .Assertion ).init (allocator );
299+ defer assertions .deinit ();
300+
301+ // Status starts with "2"
302+ try assertions .append (HttpParser.Assertion {
303+ .key = "status" ,
304+ .value = "2" ,
305+ .assertion_type = .starts_with ,
306+ });
307+ // Body starts with "Hello"
308+ try assertions .append (HttpParser.Assertion {
309+ .key = "body" ,
310+ .value = "Hello" ,
311+ .assertion_type = .starts_with ,
312+ });
313+ // Header starts with "application"
314+ try assertions .append (HttpParser.Assertion {
315+ .key = "header[\" content-type\" ]" ,
316+ .value = "application" ,
317+ .assertion_type = .starts_with ,
318+ });
319+
320+ var request = HttpParser.HttpRequest {
321+ .method = .GET ,
322+ .url = "https://api.example.com" ,
323+ .headers = std .ArrayList (http .Header ).init (allocator ),
324+ .assertions = assertions ,
325+ .body = null ,
326+ };
327+
328+ var response_headers = std .StringHashMap ([]const u8 ).init (allocator );
329+ try response_headers .put ("content-type" , "application/json" );
330+ defer response_headers .deinit ();
331+
332+ const body = try allocator .dupe (u8 , "Hello world!" );
333+ defer allocator .free (body );
334+ const response = Client.HttpResponse {
335+ .status = http .Status .ok ,
336+ .headers = response_headers ,
337+ .body = body ,
338+ .allocator = allocator ,
339+ };
340+
341+ try check (& request , response );
342+ }
0 commit comments