cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
test_client.c
Go to the documentation of this file.
1
7#include <string.h>
8#include <strings.h>
9#include <stdlib.h>
10#include <stdio.h>
11#include <ctype.h>
12
17static char *clone_str(const char *s) {
18 if (!s) return NULL;
19 size_t n = strlen(s) + 1;
20 char *p = (char *)cwist_alloc(n);
21 if (p) memcpy(p, s, n);
22 return p;
23}
24
28static void free_cookie(cwist_test_client_cookie *c) {
29 if (!c) return;
30 cwist_free(c->name);
31 cwist_free(c->value);
32 cwist_free(c->path);
34}
35
41static cwist_test_client_cookie *find_cookie(cwist_test_client_cookie *head, const char *name) {
42 if (!name) return NULL;
43 for (; head; head = head->next) {
44 if (head->name && strcmp(head->name, name) == 0) return head;
45 }
46 return NULL;
47}
48
56static void cookie_jar_apply(cwist_test_client *client, cwist_http_request *req,
57 const cwist_test_client_kv *adhoc, size_t adhoc_count) {
58 if (!client || !req) return;
59 size_t jar_count = 0;
60 for (cwist_test_client_cookie *c = client->cookies; c; c = c->next) {
61 if (c->name && c->value) jar_count++;
62 }
63 size_t valid_adhoc = 0;
64 for (size_t i = 0; i < adhoc_count; i++) {
65 if (adhoc && adhoc[i].key && adhoc[i].value) valid_adhoc++;
66 }
67 if (jar_count == 0 && valid_adhoc == 0) return;
68
69 size_t buf_len = 0;
70 for (cwist_test_client_cookie *c = client->cookies; c; c = c->next) {
71 if (c->name && c->value) buf_len += strlen(c->name) + 1 + strlen(c->value) + 2;
72 }
73 for (size_t i = 0; i < adhoc_count; i++) {
74 if (adhoc && adhoc[i].key && adhoc[i].value)
75 buf_len += strlen(adhoc[i].key) + 1 + strlen(adhoc[i].value) + 2;
76 }
77
78 char *cookie_header CWIST_DEFER_FREE = (char *)cwist_alloc(buf_len + 1);
79 if (!cookie_header) return;
80 cookie_header[0] = '\0';
81
82 size_t pos = 0;
83 for (cwist_test_client_cookie *c = client->cookies; c; c = c->next) {
84 if (!c->name || !c->value) continue;
85 if (pos > 0) cookie_header[pos++] = ';';
86 if (pos > 0) cookie_header[pos++] = ' ';
87 pos += (size_t)snprintf(cookie_header + pos, buf_len + 1 - pos, "%s=%s", c->name, c->value);
88 }
89 for (size_t i = 0; i < adhoc_count; i++) {
90 if (!adhoc || !adhoc[i].key || !adhoc[i].value) continue;
91 if (pos > 0) cookie_header[pos++] = ';';
92 if (pos > 0) cookie_header[pos++] = ' ';
93 pos += (size_t)snprintf(cookie_header + pos, buf_len + 1 - pos, "%s=%s", adhoc[i].key,
94 adhoc[i].value);
95 }
97}
98
103static char *trim(char *s) {
104 while (*s && isspace((unsigned char)*s)) s++;
105 char *e = s + strlen(s);
106 while (e > s && isspace((unsigned char)e[-1])) *--e = '\0';
107 return s;
108}
109
116static void cookie_jar_update(cwist_test_client *client, cwist_http_response *res) {
117 if (!client || !res) return;
118 for (cwist_http_header_node *h = res->headers; h; h = h->next) {
119 if (!h->key || !h->value || strcasecmp(h->key->data, "Set-Cookie") != 0) continue;
120 char *buf = clone_str(h->value->data);
121 if (!buf) continue;
122 char *saveptr = NULL;
123 char *name_val = strtok_r(buf, ";", &saveptr);
124 if (!name_val) {
125 cwist_free(buf);
126 continue;
127 }
128 name_val = trim(name_val);
129 char *eq = strchr(name_val, '=');
130 if (!eq) {
131 cwist_free(buf);
132 continue;
133 }
134 *eq = '\0';
135 char *name = clone_str(trim(name_val));
136 char *value = clone_str(trim(eq + 1));
137 if (!name || !value) {
138 cwist_free(name);
139 cwist_free(value);
140 cwist_free(buf);
141 continue;
142 }
143 char *path = NULL;
144 char *rest = strtok_r(NULL, ";", &saveptr);
145 while (rest) {
146 rest = trim(rest);
147 if (strncasecmp(rest, "Path=", 5) == 0) {
148 path = clone_str(trim(rest + 5));
149 break;
150 }
151 rest = strtok_r(NULL, ";", &saveptr);
152 }
153 cwist_test_client_cookie *c = find_cookie(client->cookies, name);
154 if (c) {
155 cwist_free(c->value);
156 c->value = value;
157 if (path) {
158 cwist_free(c->path);
159 c->path = path;
160 } else {
161 cwist_free(path);
162 }
163 cwist_free(name);
164 } else {
166 if (c) {
167 c->name = name;
168 c->value = value;
169 c->path = path;
170 c->next = client->cookies;
171 client->cookies = c;
172 } else {
173 cwist_free(name);
174 cwist_free(value);
175 cwist_free(path);
176 }
177 }
178 cwist_free(buf);
179 }
180}
181
188 if (!app) return NULL;
190 if (!client) return NULL;
191 client->app = app;
192 client->cookies = NULL;
193 return client;
194}
195
200 if (!client) return;
202 cwist_free(client);
203}
204
216static cwist_http_response *do_request(cwist_test_client *client, cwist_http_method_t method,
217 const char *path,
219 if (!client || !client->app || !path) return NULL;
221 if (!req) return NULL;
222 req->method = method;
223
224 const char *query = opts && opts->query_string ? opts->query_string : NULL;
225 const char *hash = strchr(path, '?');
226 if (hash) {
227 size_t path_len = (size_t)(hash - path);
228 char *path_only CWIST_DEFER_FREE = (char *)cwist_alloc(path_len + 1);
229 if (!path_only) {
231 return NULL;
232 }
233 memcpy(path_only, path, path_len);
234 path_only[path_len] = '\0';
236 query = hash + 1;
237 } else {
238 cwist_sstring_assign(req->path, (char *)path);
239 }
240 if (query) {
241 cwist_sstring_assign(req->query, (char *)query);
245 }
246
247 const char *body = opts ? opts->body : NULL;
248 const char *content_type = opts ? opts->content_type : NULL;
249
250 if (body) {
251 if (opts && opts->body_len > 0) {
252 cwist_sstring_assign_len(req->body, (char *)body, opts->body_len);
253 } else {
254 cwist_sstring_assign(req->body, (char *)body);
255 }
256 }
257 if (content_type) cwist_http_header_add(&req->headers, "Content-Type", content_type);
258
259 if (opts && opts->headers) {
260 for (size_t i = 0; i < opts->header_count; i++) {
261 if (opts->headers[i].key && opts->headers[i].value)
262 cwist_http_header_add(&req->headers, opts->headers[i].key, opts->headers[i].value);
263 }
264 }
265
266 cookie_jar_apply(client, req, opts ? opts->cookies : NULL, opts ? opts->cookie_count : 0);
267
269 if (!res) {
271 return NULL;
272 }
273 cwist_app_dispatch(client->app, req, res);
274 cookie_jar_update(client, res);
276 return res;
277}
278
287 cwist_http_method_t method, const char *path,
289 return do_request(client, method, path, opts);
290}
291
298 return do_request(client, CWIST_HTTP_GET, path, NULL);
299}
300
308 const char *body) {
310 opts.body = body;
311 return do_request(client, CWIST_HTTP_POST, path, &opts);
312}
313
321 const char *json_body) {
323 opts.body = json_body;
324 opts.content_type = "application/json";
325 return do_request(client, CWIST_HTTP_POST, path, &opts);
326}
327
335 const char *body) {
337 opts.body = body;
338 return do_request(client, CWIST_HTTP_PUT, path, &opts);
339}
340
347 return do_request(client, CWIST_HTTP_DELETE, path, NULL);
348}
349
357 const char *body) {
359 opts.body = body;
360 return do_request(client, CWIST_HTTP_PATCH, path, &opts);
361}
362
376 const char *field_name, const char *file_name,
377 const char *content_type, const char *data,
378 size_t data_len) {
379 if (!client || !client->app || !path || !field_name || !file_name || !content_type || !data)
380 return NULL;
381 const char *boundary = "CWISTTestClientBoundary7MA4YWxkTrZu0gW";
382 size_t boundary_len = strlen(boundary);
383
384 size_t body_len = 2 + boundary_len + 2 + 38 + strlen(field_name) + 25 + strlen(file_name) + 16 +
385 strlen(content_type) + 4 + data_len + 2 + boundary_len + 2 + 2;
386 char *body = (char *)cwist_alloc(body_len + 1);
387 if (!body) return NULL;
388
389 size_t pos = 0;
390 pos += (size_t)snprintf(body + pos, body_len + 1 - pos, "--%s\r\n", boundary);
391 pos += (size_t)snprintf(body + pos, body_len + 1 - pos,
392 "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n",
394 pos +=
395 (size_t)snprintf(body + pos, body_len + 1 - pos, "Content-Type: %s\r\n\r\n", content_type);
396 memcpy(body + pos, data, data_len);
397 pos += data_len;
398 pos += (size_t)snprintf(body + pos, body_len + 1 - pos, "\r\n--%s--\r\n", boundary);
399 body[pos] = '\0';
400
401 char ct[256];
402 snprintf(ct, sizeof(ct), "multipart/form-data; boundary=%s", boundary);
403
405 opts.body = body;
406 opts.body_len = body_len;
407 opts.content_type = ct;
408 cwist_http_response *res = do_request(client, CWIST_HTTP_POST, path, &opts);
409 cwist_free(body);
410 return res;
411}
412
422void cwist_test_client_set_cookie(cwist_test_client *client, const char *name, const char *value,
423 const char *path) {
424 if (!client || !name) return;
425 cwist_test_client_cookie *c = find_cookie(client->cookies, name);
426 if (c) {
427 cwist_free(c->value);
428 c->value = value ? clone_str(value) : NULL;
429 if (path) {
430 cwist_free(c->path);
431 c->path = clone_str(path);
432 }
433 } else {
435 if (!c) return;
436 c->name = clone_str(name);
437 c->value = value ? clone_str(value) : NULL;
438 c->path = path ? clone_str(path) : NULL;
439 c->next = client->cookies;
440 client->cookies = c;
441 }
442}
443
450const char *cwist_test_client_get_cookie(cwist_test_client *client, const char *name) {
451 if (!client || !name) return NULL;
452 cwist_test_client_cookie *c = find_cookie(client->cookies, name);
453 return c ? c->value : NULL;
454}
455
460 if (!client) return;
462 while (c) {
464 free_cookie(c);
465 c = next;
466 }
467 client->cookies = NULL;
468}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
#define CWIST_DEFER_FREE
Attach to a pointer variable's declaration to cwist_free() it automatically when that variable's encl...
Definition alloc.h:101
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
Safe string type with helper methods.
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_http_request_destroy(cwist_http_request *req)
Destroy an HTTP request object.
Definition http.c:2180
cwist_http_method_t
Definition http.h:16
@ CWIST_HTTP_PUT
Definition http.h:19
@ CWIST_HTTP_PATCH
Definition http.h:21
@ CWIST_HTTP_POST
Definition http.h:18
@ CWIST_HTTP_GET
Definition http.h:17
@ CWIST_HTTP_DELETE
Definition http.h:20
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
cwist_http_response * cwist_http_response_create(void)
Create a new HTTP response object.
Definition http.c:2291
cwist_http_request * cwist_http_request_create(void)
Create a new HTTP request object.
Definition http.c:2056
void cwist_query_map_clear(cwist_query_map *map)
Clear all entries.
Definition query.c:93
cwist_query_map * cwist_query_map_create(void)
Create a new query map.
Definition query.c:54
void cwist_query_map_parse(cwist_query_map *map, const char *raw_query)
Parse raw query strings (e.g., "a=1&b=2") into the map.
Definition query.c:319
size_t head
Definition otel.c:57
size_t cap
Definition otel.c:56
Main Application Context.
Definition app.h:155
Linked list for headers to handle multiple headers easily.
Definition http.h:59
cwist_query_map * query_params
Parsed query parameters.
Definition http.h:69
cwist_sstring * path
e.g., "/users/1"
Definition http.h:67
cwist_sstring * query
e.g., "active=true" (raw)
Definition http.h:68
cwist_sstring * body
Definition http.h:73
cwist_http_header_node * headers
Definition http.h:72
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_http_header_node * headers
Definition http.h:86
A single key/value pair for headers or ad-hoc cookies.
Definition test_client.h:25
const char * value
Definition test_client.h:27
Optional parameters for advanced test requests.
Definition test_client.h:31
const cwist_test_client_kv * cookies
Definition test_client.h:37
const cwist_test_client_kv * headers
Definition test_client.h:35
cwist_app * app
Definition test_client.h:20
cwist_test_client_cookie * cookies
Definition test_client.h:21
void cwist_app_dispatch(cwist_app *app, cwist_http_request *req, cwist_http_response *res)
Dispatch a request internally through the app's router and middleware. Used by the test client and ad...
Definition app.c:2516
cwist_http_response * cwist_test_client_post_json(cwist_test_client *client, const char *path, const char *json_body)
Send a POST request with a JSON body and application/json content type.
cwist_http_response * cwist_test_client_post_multipart(cwist_test_client *client, const char *path, const char *field_name, const char *file_name, const char *content_type, const char *data, size_t data_len)
Send a POST request with a single-file multipart/form-data body. Builds the multipart body with a fix...
cwist_http_response * cwist_test_client_put(cwist_test_client *client, const char *path, const char *body)
Send a PUT request with a plain-text body.
cwist_http_response * cwist_test_client_get(cwist_test_client *client, const char *path)
Send a GET request.
void cwist_test_client_clear_cookies(cwist_test_client *client)
Remove and free all cookies in the client's jar.
cwist_http_response * cwist_test_client_delete(cwist_test_client *client, const char *path)
Send a DELETE request.
void cwist_test_client_destroy(cwist_test_client *client)
Destroy a test client and free all its cookies.
cwist_http_response * cwist_test_client_request_ex(cwist_test_client *client, cwist_http_method_t method, const char *path, const cwist_test_client_request_options *opts)
Send a request with explicit method and options.
cwist_http_response * cwist_test_client_patch(cwist_test_client *client, const char *path, const char *body)
Send a PATCH request with a plain-text body.
void cwist_test_client_set_cookie(cwist_test_client *client, const char *name, const char *value, const char *path)
Set or replace a cookie in the client's jar. A cookie with the same name gets its value replaced (and...
const char * cwist_test_client_get_cookie(cwist_test_client *client, const char *name)
Get the value of a cookie from the client's jar.
cwist_test_client * cwist_test_client_create(cwist_app *app)
Create a test HTTP client bound to an app.
cwist_http_response * cwist_test_client_post(cwist_test_client *client, const char *path, const char *body)
Send a POST request with a plain-text body.
In-process HTTP test client (ala Flask test_client).