HIGH: - H1: enlarge test base_with_slash buffer 64 → 80; cmake --build now clean under -Werror=format-truncation. - H2: CURLOPT_FOLLOWLOCATION = 0 (no cross-host bearer leak; SDK talks to a known endpoint, redirects unexpected). MAXREDIRS dropped. - H3: cf_admin_revoke_token validates name [A-Za-z0-9_-]+ client-side before URL build; rejects "a/../healthz" with CF_ERR_USAGE before the request leaves the process. MEDIUM: - M1: cf_buf_append overflow guards — n + len + 1 wrap-check up front; newcap *= 2 doubling-loop bounded by SIZE_MAX/2. - M2: 64 MiB CF_MAX_RESPONSE_BYTES cap exposed on the public header; write_cb aborts the transfer once exceeded → CF_ERR_TRANSPORT. - M3: CURLOPT_CONNECTTIMEOUT_MS = 10000 (was implicit 300s default). - M4: g_curl_init_count is now _Atomic int (C11 stdatomic) using atomic_fetch_add/sub; concurrent cf_client_new/cf_client_free across threads no longer races the libcurl global init/cleanup transition. LOW: - L1: push_auth propagates CF_ERR_OOM via an out-param instead of silently dropping the Authorization header (which previously surfaced as a misleading 401 from the server). - L2: write_cb size*nmemb overflow defensive guard. CVE: - Bump vendored cJSON 1.7.15 → 1.7.18 (fixes CVE-2024-31755: cJSON_SetValuestring NULL-deref). cJSON.c/cJSON.h replaced from upstream tag v1.7.18; LICENSE file unchanged. README updated. Tests added (15 → 21): - test_revoke_token_validates_name: path-traversal name rejected, valid name proceeds through to transport. - test_buf_append_overflow_guards: synthetic SIZE_MAX-edge inputs trigger error-return rather than wrap. - test_response_body_size_cap: mock streams 65 MiB; client aborts with CF_ERR_TRANSPORT. - test_connect_timeout: dial 10.255.255.1, assert <18s wallclock (vs. libcurl's 300s default). - test_concurrent_client_init: 4 pthreads × 50 iters, no crash, no leak under valgrind. - test_cjson_bump: cJSON_SetValuestring(node, NULL) returns NULL safely; malformed cJSON_Parse returns NULL. Verification: - cmake --build build (Release): clean - ctest --test-dir build: 21/21 pass (incl. 10s connect-timeout test) - ctest --test-dir build-asan (ASan + UBSan): clean - valgrind --leak-check=full: 10,313 allocs == 10,313 frees, 0 errors, 0 leaks README updated: cJSON 1.7.18 note, C11 + stdatomic requirement. Audit: memory/clawdforge-audits/c-a69e924.md
1047 lines
35 KiB
C
1047 lines
35 KiB
C
/* Test suite for the clawdforge C SDK.
|
|
*
|
|
* Strategy: spin up a tiny single-threaded HTTP server on 127.0.0.1
|
|
* inside this process. The server pulls a scripted response off a
|
|
* queue per request and returns it. No external dependency beyond
|
|
* the platform sockets API and pthreads (already a transitive dep
|
|
* of libcurl on Linux).
|
|
*
|
|
* We test the wire surface of the client end-to-end through libcurl:
|
|
* URL construction, auth header injection, JSON request shape,
|
|
* JSON response parsing, error envelopes, multipart upload.
|
|
*/
|
|
|
|
#define _DEFAULT_SOURCE
|
|
#define _GNU_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <arpa/inet.h>
|
|
#include <ctype.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
|
|
#include <clawdforge.h>
|
|
#include "cJSON.h"
|
|
|
|
/* Internal symbols used by the buffer-overflow guard test. The static
|
|
* library exports them; these are not part of the public ABI. */
|
|
typedef struct cf_buf {
|
|
char *data;
|
|
size_t len;
|
|
size_t cap;
|
|
} cf_buf_t_test;
|
|
extern void cf_buf_init(cf_buf_t_test *b);
|
|
extern int cf_buf_append(cf_buf_t_test *b, const void *src, size_t n);
|
|
extern void cf_buf_free(cf_buf_t_test *b);
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* tiny test harness */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int g_failures = 0;
|
|
static int g_tests = 0;
|
|
static const char *g_current = "(none)";
|
|
|
|
#define TEST(name) do { g_current = name; g_tests++; } while (0)
|
|
#define CHECK(cond) do { \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, "FAIL [%s] %s:%d: %s\n", \
|
|
g_current, __FILE__, __LINE__, #cond); \
|
|
g_failures++; \
|
|
} \
|
|
} while (0)
|
|
/* Portable case-insensitive prefix compare. Returns 0 on match. */
|
|
static int header_match(const char *line, const char *name) {
|
|
size_t n = strlen(name);
|
|
for (size_t i = 0; i < n; ++i) {
|
|
if (!line[i]) return 1;
|
|
if (tolower((unsigned char)line[i]) != tolower((unsigned char)name[i])) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Portable memmem (avoids _GNU_SOURCE dep). */
|
|
static const void *find_bytes(const void *hay, size_t hay_len,
|
|
const void *needle, size_t need_len) {
|
|
if (need_len == 0) return hay;
|
|
if (hay_len < need_len) return NULL;
|
|
const char *h = (const char *)hay;
|
|
const char *n = (const char *)needle;
|
|
for (size_t i = 0; i + need_len <= hay_len; ++i) {
|
|
if (memcmp(h + i, n, need_len) == 0) return h + i;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#define CHECK_STR_EQ(a, b) do { \
|
|
const char *_a = (a); const char *_b = (b); \
|
|
if (!_a || !_b || strcmp(_a, _b) != 0) { \
|
|
fprintf(stderr, "FAIL [%s] %s:%d: \"%s\" != \"%s\"\n", \
|
|
g_current, __FILE__, __LINE__, \
|
|
_a ? _a : "(null)", _b ? _b : "(null)"); \
|
|
g_failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* mock server */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef struct mock_response {
|
|
int status;
|
|
char *content_type;
|
|
char *body;
|
|
/* When stream_bytes > 0, the response body is `stream_bytes` ASCII
|
|
* 'x' characters generated on the fly from a small fixed buffer
|
|
* (no big malloc). Used to exercise the response-body cap without
|
|
* allocating tens of megabytes server-side. */
|
|
long stream_bytes;
|
|
} mock_response_t;
|
|
|
|
#define MAX_RESPONSES 16
|
|
typedef struct mock_request {
|
|
char method[16];
|
|
char path[256];
|
|
char *auth; /* "Bearer ..." or NULL */
|
|
char *content_type;
|
|
char *body;
|
|
size_t body_len;
|
|
} mock_request_t;
|
|
|
|
static int srv_fd = -1;
|
|
static int srv_port = 0;
|
|
static pthread_t srv_thread;
|
|
static volatile int srv_stop = 0;
|
|
|
|
static pthread_mutex_t q_mu = PTHREAD_MUTEX_INITIALIZER;
|
|
static mock_response_t q_resp[MAX_RESPONSES];
|
|
static int q_resp_head = 0;
|
|
static int q_resp_tail = 0;
|
|
|
|
static mock_request_t captured[MAX_RESPONSES];
|
|
static int captured_count = 0;
|
|
|
|
static void enqueue_response(int status, const char *content_type, const char *body) {
|
|
pthread_mutex_lock(&q_mu);
|
|
mock_response_t *r = &q_resp[q_resp_tail];
|
|
r->status = status;
|
|
r->content_type = content_type ? strdup(content_type) : strdup("application/json");
|
|
r->body = body ? strdup(body) : strdup("");
|
|
r->stream_bytes = 0;
|
|
q_resp_tail = (q_resp_tail + 1) % MAX_RESPONSES;
|
|
pthread_mutex_unlock(&q_mu);
|
|
}
|
|
|
|
static void enqueue_stream_response(int status, long bytes) {
|
|
pthread_mutex_lock(&q_mu);
|
|
mock_response_t *r = &q_resp[q_resp_tail];
|
|
r->status = status;
|
|
r->content_type = strdup("application/octet-stream");
|
|
r->body = NULL;
|
|
r->stream_bytes = bytes;
|
|
q_resp_tail = (q_resp_tail + 1) % MAX_RESPONSES;
|
|
pthread_mutex_unlock(&q_mu);
|
|
}
|
|
|
|
static int dequeue_response(mock_response_t *out) {
|
|
pthread_mutex_lock(&q_mu);
|
|
if (q_resp_head == q_resp_tail) {
|
|
pthread_mutex_unlock(&q_mu);
|
|
return -1;
|
|
}
|
|
*out = q_resp[q_resp_head];
|
|
q_resp_head = (q_resp_head + 1) % MAX_RESPONSES;
|
|
pthread_mutex_unlock(&q_mu);
|
|
return 0;
|
|
}
|
|
|
|
static void capture(const mock_request_t *r) {
|
|
pthread_mutex_lock(&q_mu);
|
|
if (captured_count < MAX_RESPONSES) {
|
|
captured[captured_count++] = *r;
|
|
}
|
|
pthread_mutex_unlock(&q_mu);
|
|
}
|
|
|
|
/* Read the full HTTP request from a socket. Returns 0 on success. */
|
|
static int read_request(int fd, mock_request_t *req) {
|
|
char buf[64 * 1024];
|
|
size_t off = 0;
|
|
/* Read until headers complete */
|
|
while (off < sizeof(buf) - 1) {
|
|
ssize_t n = read(fd, buf + off, sizeof(buf) - 1 - off);
|
|
if (n <= 0) return -1;
|
|
off += (size_t)n;
|
|
buf[off] = '\0';
|
|
if (strstr(buf, "\r\n\r\n")) break;
|
|
}
|
|
char *blank_line = strstr(buf, "\r\n\r\n");
|
|
if (!blank_line) return -1;
|
|
char *body_start = blank_line + 4;
|
|
size_t header_len = (size_t)(body_start - buf);
|
|
/* The last header's terminating "\r\n" is the FIRST "\r\n" of the
|
|
* "\r\n\r\n" sequence — so it's at blank_line..+1. To process every
|
|
* header line including the last one, walk byte-range
|
|
* [buf .. blank_line + 2). The "+2" includes the last header's "\r\n". */
|
|
char *header_end = blank_line + 2;
|
|
|
|
/* Method + path */
|
|
char *line_end = strstr(buf, "\r\n");
|
|
if (!line_end || line_end >= header_end) return -1;
|
|
char first_line[1024];
|
|
size_t fl_len = (size_t)(line_end - buf);
|
|
if (fl_len >= sizeof first_line) fl_len = sizeof first_line - 1;
|
|
memcpy(first_line, buf, fl_len);
|
|
first_line[fl_len] = '\0';
|
|
char path_buf[256] = {0};
|
|
if (sscanf(first_line, "%15s %255s", req->method, path_buf) != 2) return -1;
|
|
memcpy(req->path, path_buf, sizeof req->path);
|
|
req->path[sizeof req->path - 1] = '\0';
|
|
|
|
/* Headers */
|
|
req->auth = NULL;
|
|
req->content_type = NULL;
|
|
long content_length = 0;
|
|
char *p = line_end + 2;
|
|
while (p < header_end) {
|
|
/* find next \r\n strictly within the header range */
|
|
char *nl = NULL;
|
|
for (char *q = p; q + 1 < header_end; ++q) {
|
|
if (q[0] == '\r' && q[1] == '\n') { nl = q; break; }
|
|
}
|
|
if (!nl) break;
|
|
size_t llen = (size_t)(nl - p);
|
|
char line[2048];
|
|
if (llen >= sizeof line) llen = sizeof line - 1;
|
|
memcpy(line, p, llen);
|
|
line[llen] = '\0';
|
|
|
|
if (header_match(line, "Authorization:") == 0) {
|
|
char *v = line + 14;
|
|
while (*v == ' ') v++;
|
|
req->auth = strdup(v);
|
|
} else if (header_match(line, "Content-Type:") == 0) {
|
|
char *v = line + 13;
|
|
while (*v == ' ') v++;
|
|
req->content_type = strdup(v);
|
|
} else if (header_match(line, "Content-Length:") == 0) {
|
|
content_length = strtol(line + 15, NULL, 10);
|
|
}
|
|
p = nl + 2;
|
|
}
|
|
|
|
/* Body */
|
|
req->body = NULL;
|
|
req->body_len = 0;
|
|
if (content_length > 0) {
|
|
size_t already = off - header_len;
|
|
char *body = (char *)malloc((size_t)content_length + 1);
|
|
if (!body) return -1;
|
|
if (already > (size_t)content_length) already = (size_t)content_length;
|
|
memcpy(body, body_start, already);
|
|
size_t got = already;
|
|
while (got < (size_t)content_length) {
|
|
ssize_t n = read(fd, body + got, (size_t)content_length - got);
|
|
if (n <= 0) { free(body); return -1; }
|
|
got += (size_t)n;
|
|
}
|
|
body[content_length] = '\0';
|
|
req->body = body;
|
|
req->body_len = (size_t)content_length;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void write_response(int fd, const mock_response_t *resp) {
|
|
size_t body_len = resp->stream_bytes > 0
|
|
? (size_t)resp->stream_bytes
|
|
: (resp->body ? strlen(resp->body) : 0);
|
|
char header[512];
|
|
int n = snprintf(header, sizeof header,
|
|
"HTTP/1.1 %d %s\r\n"
|
|
"Content-Type: %s\r\n"
|
|
"Content-Length: %zu\r\n"
|
|
"Connection: close\r\n"
|
|
"\r\n",
|
|
resp->status,
|
|
resp->status == 200 ? "OK" :
|
|
resp->status == 401 ? "Unauthorized" :
|
|
resp->status == 404 ? "Not Found" :
|
|
resp->status == 502 ? "Bad Gateway" : "Other",
|
|
resp->content_type ? resp->content_type : "application/json",
|
|
body_len);
|
|
if (n > 0) {
|
|
ssize_t wrote = write(fd, header, (size_t)n);
|
|
(void)wrote;
|
|
if (resp->stream_bytes > 0) {
|
|
/* Stream `stream_bytes` ASCII 'x' from a small chunk buffer.
|
|
* Stops early on EPIPE — clients that abort write_cb hang up
|
|
* mid-transfer, which is exactly what we want to test. */
|
|
char chunk[8192];
|
|
memset(chunk, 'x', sizeof chunk);
|
|
size_t remaining = (size_t)resp->stream_bytes;
|
|
while (remaining > 0) {
|
|
size_t step = remaining < sizeof chunk ? remaining : sizeof chunk;
|
|
ssize_t w = write(fd, chunk, step);
|
|
if (w <= 0) break;
|
|
remaining -= (size_t)w;
|
|
}
|
|
} else if (resp->body && *resp->body) {
|
|
wrote = write(fd, resp->body, strlen(resp->body));
|
|
(void)wrote;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void *server_loop(void *arg) {
|
|
(void)arg;
|
|
while (!srv_stop) {
|
|
struct sockaddr_in addr;
|
|
socklen_t alen = sizeof addr;
|
|
int cfd = accept(srv_fd, (struct sockaddr *)&addr, &alen);
|
|
if (cfd < 0) {
|
|
if (errno == EINTR || errno == EAGAIN) continue;
|
|
break;
|
|
}
|
|
mock_request_t req;
|
|
memset(&req, 0, sizeof req);
|
|
if (read_request(cfd, &req) == 0) {
|
|
capture(&req);
|
|
mock_response_t resp;
|
|
if (dequeue_response(&resp) == 0) {
|
|
write_response(cfd, &resp);
|
|
free(resp.content_type);
|
|
free(resp.body);
|
|
} else {
|
|
/* No script left — return 500. */
|
|
mock_response_t fb = {500, strdup("text/plain"), strdup("no scripted response"), 0};
|
|
write_response(cfd, &fb);
|
|
free(fb.content_type);
|
|
free(fb.body);
|
|
}
|
|
}
|
|
close(cfd);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int start_server(void) {
|
|
srv_fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (srv_fd < 0) return -1;
|
|
int yes = 1;
|
|
setsockopt(srv_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
|
|
|
|
/* Bind to a random port on loopback. */
|
|
struct sockaddr_in addr = {0};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = 0;
|
|
if (bind(srv_fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -1;
|
|
|
|
socklen_t alen = sizeof addr;
|
|
if (getsockname(srv_fd, (struct sockaddr *)&addr, &alen) != 0) return -1;
|
|
srv_port = ntohs(addr.sin_port);
|
|
|
|
if (listen(srv_fd, 16) != 0) return -1;
|
|
|
|
/* Make accept interruptible by close. */
|
|
int flags = fcntl(srv_fd, F_GETFL, 0);
|
|
(void)flags;
|
|
|
|
if (pthread_create(&srv_thread, NULL, server_loop, NULL) != 0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static void stop_server(void) {
|
|
srv_stop = 1;
|
|
/* Kick accept by connecting + closing. */
|
|
int kicker = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (kicker >= 0) {
|
|
struct sockaddr_in addr = {0};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = htons(srv_port);
|
|
connect(kicker, (struct sockaddr *)&addr, sizeof addr);
|
|
close(kicker);
|
|
}
|
|
pthread_join(srv_thread, NULL);
|
|
if (srv_fd >= 0) close(srv_fd);
|
|
/* Drain any residual queue + captures. */
|
|
pthread_mutex_lock(&q_mu);
|
|
while (q_resp_head != q_resp_tail) {
|
|
free(q_resp[q_resp_head].content_type);
|
|
free(q_resp[q_resp_head].body);
|
|
q_resp_head = (q_resp_head + 1) % MAX_RESPONSES;
|
|
}
|
|
for (int i = 0; i < captured_count; ++i) {
|
|
free(captured[i].auth);
|
|
free(captured[i].content_type);
|
|
free(captured[i].body);
|
|
}
|
|
captured_count = 0;
|
|
pthread_mutex_unlock(&q_mu);
|
|
}
|
|
|
|
static char *make_base_url(void) {
|
|
static char buf[64];
|
|
snprintf(buf, sizeof buf, "http://127.0.0.1:%d", srv_port);
|
|
return buf;
|
|
}
|
|
|
|
static void reset_captures(void) {
|
|
pthread_mutex_lock(&q_mu);
|
|
for (int i = 0; i < captured_count; ++i) {
|
|
free(captured[i].auth);
|
|
free(captured[i].content_type);
|
|
free(captured[i].body);
|
|
}
|
|
captured_count = 0;
|
|
pthread_mutex_unlock(&q_mu);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* tests */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void t_version(void) {
|
|
TEST("version");
|
|
CHECK_STR_EQ(cf_version(), "0.1.0");
|
|
CHECK_STR_EQ(cf_status_str(CF_OK), "CF_OK");
|
|
CHECK_STR_EQ(cf_status_str(CF_ERR_PARSE), "CF_ERR_PARSE");
|
|
}
|
|
|
|
static void t_client_lifecycle(void) {
|
|
TEST("client_lifecycle");
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1/", "tok");
|
|
CHECK(c != NULL);
|
|
cf_client_set_timeout_secs(c, 30);
|
|
CHECK(cf_client_set_admin_token(c, "admintok") == CF_OK);
|
|
cf_client_free(c);
|
|
|
|
/* NULL base_url rejected. */
|
|
CHECK(cf_client_new(NULL, "x") == NULL);
|
|
CHECK(cf_client_new("", "x") == NULL);
|
|
}
|
|
|
|
static void t_healthz_ok(void) {
|
|
TEST("healthz_ok");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"ok\":true,\"claude_present\":true,\"claude_version\":\"1.2.3\"}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_client_set_timeout_secs(c, 5);
|
|
cf_healthz_t h = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_healthz(c, &h, &err);
|
|
CHECK(s == CF_OK);
|
|
CHECK(h.ok == 1);
|
|
CHECK(h.claude_present == 1);
|
|
CHECK_STR_EQ(h.claude_version, "1.2.3");
|
|
cf_healthz_free(&h);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
|
|
/* /healthz must NOT have an Auth header — server says it doesn't
|
|
* require one and the client doesn't promise one. (Our code does
|
|
* inject the bearer token when set; that's also fine — server
|
|
* ignores. Just verify the path was correct.) */
|
|
CHECK_STR_EQ(captured[0].method, "GET");
|
|
CHECK_STR_EQ(captured[0].path, "/healthz");
|
|
}
|
|
|
|
static void t_healthz_transport_fail(void) {
|
|
TEST("healthz_transport_fail");
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1", "tok");
|
|
cf_client_set_timeout_secs(c, 2);
|
|
cf_healthz_t h = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_healthz(c, &h, &err);
|
|
CHECK(s == CF_ERR_TRANSPORT);
|
|
CHECK(err.message && err.message[0] != '\0');
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_success_object(void) {
|
|
TEST("run_success_object");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"ok\":true,"
|
|
"\"result\":{\"hello\":\"world\",\"n\":7},"
|
|
"\"duration_ms\":1234,"
|
|
"\"stop_reason\":\"end_turn\"}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "cf_abc");
|
|
cf_run_request_t req = {
|
|
.prompt = "Reply with JSON",
|
|
.model = "sonnet",
|
|
.timeout_secs = 30,
|
|
};
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_run(c, &req, &res, &err);
|
|
CHECK(s == CF_OK);
|
|
CHECK(res.duration_ms == 1234);
|
|
CHECK(res.result_is_string == 0);
|
|
CHECK_STR_EQ(res.stop_reason, "end_turn");
|
|
CHECK(res.result_json != NULL);
|
|
|
|
/* result_json should be parseable by cJSON. */
|
|
cJSON *parsed = (cJSON *)cf_run_result_as_cjson(&res);
|
|
CHECK(parsed != NULL);
|
|
cJSON *hello = cJSON_GetObjectItemCaseSensitive(parsed, "hello");
|
|
CHECK(cJSON_IsString(hello));
|
|
if (cJSON_IsString(hello)) CHECK_STR_EQ(hello->valuestring, "world");
|
|
cJSON_Delete(parsed);
|
|
|
|
/* Verify request body sent the right shape. */
|
|
CHECK_STR_EQ(captured[0].method, "POST");
|
|
CHECK_STR_EQ(captured[0].path, "/run");
|
|
CHECK(captured[0].auth != NULL);
|
|
CHECK_STR_EQ(captured[0].auth, "Bearer cf_abc");
|
|
cJSON *sent = cJSON_Parse(captured[0].body);
|
|
CHECK(sent != NULL);
|
|
if (sent) {
|
|
cJSON *prompt = cJSON_GetObjectItemCaseSensitive(sent, "prompt");
|
|
cJSON *model = cJSON_GetObjectItemCaseSensitive(sent, "model");
|
|
cJSON *to = cJSON_GetObjectItemCaseSensitive(sent, "timeout_secs");
|
|
CHECK(cJSON_IsString(prompt));
|
|
if (cJSON_IsString(prompt)) CHECK_STR_EQ(prompt->valuestring, "Reply with JSON");
|
|
CHECK(cJSON_IsString(model));
|
|
if (cJSON_IsString(model)) CHECK_STR_EQ(model->valuestring, "sonnet");
|
|
CHECK(cJSON_IsNumber(to));
|
|
if (cJSON_IsNumber(to)) CHECK((int)to->valuedouble == 30);
|
|
cJSON_Delete(sent);
|
|
}
|
|
|
|
cf_run_result_free(&res);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_success_string(void) {
|
|
TEST("run_success_string");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"ok\":true,"
|
|
"\"result\":\"plain text reply\","
|
|
"\"duration_ms\":42,"
|
|
"\"stop_reason\":\"end_turn\"}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_run_request_t req = { .prompt = "hi" };
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
CHECK(cf_run(c, &req, &res, &err) == CF_OK);
|
|
CHECK(res.result_is_string == 1);
|
|
/* result_json is the JSON-encoded string, including quotes. */
|
|
CHECK_STR_EQ(res.result_json, "\"plain text reply\"");
|
|
|
|
cf_run_result_free(&res);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_files_passed(void) {
|
|
TEST("run_files_passed");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"ok\":true,\"result\":\"ok\",\"duration_ms\":1,\"stop_reason\":\"end_turn\"}");
|
|
|
|
const char *files[] = { "ff_aaa", "ff_bbb" };
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_run_request_t req = {
|
|
.prompt = "extract",
|
|
.files = files,
|
|
.files_count = 2,
|
|
};
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
CHECK(cf_run(c, &req, &res, &err) == CF_OK);
|
|
|
|
cJSON *sent = cJSON_Parse(captured[0].body);
|
|
CHECK(sent != NULL);
|
|
if (sent) {
|
|
cJSON *arr = cJSON_GetObjectItemCaseSensitive(sent, "files");
|
|
CHECK(cJSON_IsArray(arr));
|
|
if (cJSON_IsArray(arr)) {
|
|
CHECK(cJSON_GetArraySize(arr) == 2);
|
|
cJSON *e0 = cJSON_GetArrayItem(arr, 0);
|
|
cJSON *e1 = cJSON_GetArrayItem(arr, 1);
|
|
CHECK(cJSON_IsString(e0));
|
|
CHECK(cJSON_IsString(e1));
|
|
if (cJSON_IsString(e0)) CHECK_STR_EQ(e0->valuestring, "ff_aaa");
|
|
if (cJSON_IsString(e1)) CHECK_STR_EQ(e1->valuestring, "ff_bbb");
|
|
}
|
|
cJSON_Delete(sent);
|
|
}
|
|
|
|
cf_run_result_free(&res);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_502_envelope(void) {
|
|
TEST("run_502_envelope");
|
|
reset_captures();
|
|
enqueue_response(502, "application/json",
|
|
"{\"ok\":false,\"error\":\"claude exited with status 1\","
|
|
"\"stderr\":\"some stderr\",\"duration_ms\":2000,\"stop_reason\":null}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_run_request_t req = { .prompt = "x" };
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_run(c, &req, &res, &err);
|
|
CHECK(s == CF_ERR_API);
|
|
CHECK(err.http_status == 502);
|
|
CHECK(err.message != NULL);
|
|
CHECK(strstr(err.message, "claude exited") != NULL);
|
|
|
|
cf_run_result_free(&res);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_401_auth(void) {
|
|
TEST("run_401_auth");
|
|
reset_captures();
|
|
enqueue_response(401, "application/json", "{\"detail\":\"invalid token\"}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "wrong");
|
|
cf_run_request_t req = { .prompt = "x" };
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_run(c, &req, &res, &err);
|
|
CHECK(s == CF_ERR_AUTH);
|
|
CHECK(err.http_status == 401);
|
|
CHECK(err.message != NULL);
|
|
CHECK(strstr(err.message, "invalid token") != NULL);
|
|
|
|
cf_run_result_free(&res);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_run_usage_errors(void) {
|
|
TEST("run_usage_errors");
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1", "tok");
|
|
cf_run_request_t req = { .prompt = "" };
|
|
cf_run_result_t res = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_run(c, &req, &res, &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
CHECK(err.message != NULL);
|
|
cf_error_free(&err);
|
|
|
|
/* NULL request */
|
|
s = cf_run(c, NULL, &res, &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
cf_error_free(&err);
|
|
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_upload_file(void) {
|
|
TEST("upload_file");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"file_token\":\"ff_xyz\",\"ttl_secs\":600,\"size\":11}");
|
|
|
|
/* Write a tmp file. */
|
|
char tmpl[] = "/tmp/cf-c-test-XXXXXX";
|
|
int fd = mkstemp(tmpl);
|
|
CHECK(fd >= 0);
|
|
if (fd >= 0) {
|
|
ssize_t w = write(fd, "hello world", 11);
|
|
(void)w;
|
|
close(fd);
|
|
}
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_file_token_t ft = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_upload_file(c, tmpl, 600, &ft, &err);
|
|
CHECK(s == CF_OK);
|
|
CHECK_STR_EQ(ft.file_token, "ff_xyz");
|
|
CHECK(ft.ttl_secs == 600);
|
|
CHECK(ft.size == 11);
|
|
|
|
/* Verify request shape: POST /files, multipart, contains file content. */
|
|
CHECK_STR_EQ(captured[0].method, "POST");
|
|
CHECK_STR_EQ(captured[0].path, "/files");
|
|
CHECK(captured[0].content_type != NULL);
|
|
if (captured[0].content_type) {
|
|
CHECK(strstr(captured[0].content_type, "multipart/form-data") != NULL);
|
|
}
|
|
CHECK(captured[0].body != NULL);
|
|
if (captured[0].body) {
|
|
CHECK(find_bytes(captured[0].body, captured[0].body_len, "hello world", 11) != NULL);
|
|
CHECK(find_bytes(captured[0].body, captured[0].body_len, "ttl_secs", 8) != NULL);
|
|
}
|
|
|
|
cf_file_token_free(&ft);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
unlink(tmpl);
|
|
}
|
|
|
|
static void t_admin_create_token(void) {
|
|
TEST("admin_create_token");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"name\":\"cauldron\",\"token\":\"cf_secret_xxx\","
|
|
"\"ip_cidrs\":[\"172.24.0.0/16\",\"10.0.0.0/8\"]}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), NULL);
|
|
CHECK(cf_client_set_admin_token(c, "admin_bootstrap_token") == CF_OK);
|
|
|
|
const char *cidrs[] = { "172.24.0.0/16", "10.0.0.0/8" };
|
|
cf_admin_token_t out = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_admin_create_token(c, "cauldron", cidrs, 2, &out, &err);
|
|
CHECK(s == CF_OK);
|
|
CHECK_STR_EQ(out.name, "cauldron");
|
|
CHECK_STR_EQ(out.token, "cf_secret_xxx");
|
|
CHECK(out.ip_cidrs_count == 2);
|
|
if (out.ip_cidrs_count == 2) {
|
|
CHECK_STR_EQ(out.ip_cidrs[0], "172.24.0.0/16");
|
|
CHECK_STR_EQ(out.ip_cidrs[1], "10.0.0.0/8");
|
|
}
|
|
|
|
/* Verify auth was the admin token. */
|
|
CHECK_STR_EQ(captured[0].auth, "Bearer admin_bootstrap_token");
|
|
|
|
cf_admin_token_free(&out);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_admin_list_revoke(void) {
|
|
TEST("admin_list_revoke");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"tokens\":["
|
|
"{\"name\":\"a\",\"ip_cidrs\":[\"127.0.0.1/32\"],\"created_at\":1700000000,\"last_used_at\":1700000100},"
|
|
"{\"name\":\"b\",\"ip_cidrs\":[],\"created_at\":1700000200}"
|
|
"]}");
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), NULL);
|
|
cf_client_set_admin_token(c, "admin");
|
|
cf_admin_token_list_t list = {0};
|
|
cf_error_t err = {0};
|
|
CHECK(cf_admin_list_tokens(c, &list, &err) == CF_OK);
|
|
CHECK(list.count == 2);
|
|
if (list.count == 2) {
|
|
CHECK_STR_EQ(list.items[0].name, "a");
|
|
CHECK(list.items[0].ip_cidrs_count == 1);
|
|
if (list.items[0].ip_cidrs_count == 1) {
|
|
CHECK_STR_EQ(list.items[0].ip_cidrs[0], "127.0.0.1/32");
|
|
}
|
|
CHECK(list.items[0].created_at == 1700000000);
|
|
CHECK(list.items[0].last_used_at == 1700000100);
|
|
CHECK_STR_EQ(list.items[1].name, "b");
|
|
CHECK(list.items[1].ip_cidrs_count == 0);
|
|
}
|
|
cf_admin_token_list_free(&list);
|
|
cf_error_free(&err);
|
|
|
|
/* Revoke. */
|
|
enqueue_response(200, "application/json", "{\"ok\":true}");
|
|
CHECK(cf_admin_revoke_token(c, "a", &err) == CF_OK);
|
|
CHECK_STR_EQ(captured[1].method, "DELETE");
|
|
CHECK_STR_EQ(captured[1].path, "/admin/tokens/a");
|
|
|
|
/* Revoke missing -> 404 */
|
|
enqueue_response(404, "application/json", "{\"detail\":\"no such token\"}");
|
|
cf_status_t s = cf_admin_revoke_token(c, "nope", &err);
|
|
CHECK(s == CF_ERR_API);
|
|
CHECK(err.http_status == 404);
|
|
CHECK(err.message && strstr(err.message, "no such token") != NULL);
|
|
cf_error_free(&err);
|
|
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_admin_requires_token(void) {
|
|
TEST("admin_requires_token");
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1", NULL);
|
|
cf_admin_token_list_t list = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_admin_list_tokens(c, &list, &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
CHECK(err.message != NULL);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
static void t_url_normalisation(void) {
|
|
TEST("url_normalisation");
|
|
reset_captures();
|
|
enqueue_response(200, "application/json",
|
|
"{\"ok\":true,\"claude_present\":false}");
|
|
|
|
/* Trailing slash should be stripped. */
|
|
char base_with_slash[80];
|
|
snprintf(base_with_slash, sizeof base_with_slash, "%s/", make_base_url());
|
|
cf_client_t *c = cf_client_new(base_with_slash, "tok");
|
|
cf_healthz_t h = {0};
|
|
cf_error_t err = {0};
|
|
CHECK(cf_healthz(c, &h, &err) == CF_OK);
|
|
CHECK_STR_EQ(captured[0].path, "/healthz");
|
|
cf_healthz_free(&h);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* New tests added by audit-fix pass */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* H3 follow-up: cf_admin_revoke_token must reject names that are
|
|
* not [A-Za-z0-9_-]+ before it interpolates into the URL. */
|
|
static void t_revoke_token_validates_name(void) {
|
|
TEST("revoke_token_validates_name");
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1", NULL);
|
|
cf_client_set_admin_token(c, "admin");
|
|
cf_error_t err = {0};
|
|
|
|
/* Path-traversal attempt: must be rejected client-side. */
|
|
cf_status_t s = cf_admin_revoke_token(c, "a/../healthz", &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
CHECK(err.message != NULL);
|
|
if (err.message) CHECK(strstr(err.message, "invalid token name") != NULL);
|
|
cf_error_free(&err);
|
|
|
|
/* Empty name */
|
|
s = cf_admin_revoke_token(c, "", &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
cf_error_free(&err);
|
|
|
|
/* URL-encoded byte still rejected — raw byte not in allow-list. */
|
|
s = cf_admin_revoke_token(c, "a%2F..%2Fhealthz", &err);
|
|
CHECK(s == CF_ERR_USAGE);
|
|
cf_error_free(&err);
|
|
|
|
/* A valid name still proceeds to a network call (which fails because
|
|
* no server is listening at :1, surfacing CF_ERR_TRANSPORT — not
|
|
* CF_ERR_USAGE — proving the validator let it through). */
|
|
s = cf_admin_revoke_token(c, "valid-name_123", &err);
|
|
CHECK(s == CF_ERR_TRANSPORT);
|
|
cf_error_free(&err);
|
|
|
|
cf_client_free(c);
|
|
}
|
|
|
|
/* M1: cf_buf_append guards both the additive overflow (n + len + 1) and
|
|
* the doubling overflow (newcap *= 2). The guards must fire on the size
|
|
* computation alone — neither realloc nor memcpy is reached on either
|
|
* synthetic input, so data may safely stay NULL. */
|
|
static void t_buf_append_overflow_guards(void) {
|
|
TEST("buf_append_overflow_guards");
|
|
cf_buf_t_test b;
|
|
cf_buf_init(&b);
|
|
|
|
/* (a) additive overflow: n + len + 1 wraps. */
|
|
b.data = NULL;
|
|
b.cap = 0;
|
|
b.len = SIZE_MAX - 4;
|
|
int rc = cf_buf_append(&b, "abcd", 5); /* 5 > SIZE_MAX - (SIZE_MAX-4) - 1 = 3 */
|
|
CHECK(rc == -1);
|
|
|
|
/* (b) doubling overflow: cap is already past SIZE_MAX/2, and n
|
|
* forces need > cap so the loop runs. The first iteration must
|
|
* trip the `newcap > SIZE_MAX/2` guard. */
|
|
b.data = NULL;
|
|
b.len = 0;
|
|
b.cap = (SIZE_MAX / 2) + 100;
|
|
rc = cf_buf_append(&b, "x", SIZE_MAX - 200);
|
|
CHECK(rc == -1);
|
|
|
|
/* Reset to a clean state for the sanity check / free. */
|
|
b.data = NULL;
|
|
b.len = 0;
|
|
b.cap = 0;
|
|
|
|
/* (c) sanity: a normal small append still works. */
|
|
rc = cf_buf_append(&b, "hello", 5);
|
|
CHECK(rc == 0);
|
|
CHECK(b.len == 5);
|
|
CHECK(b.data != NULL);
|
|
if (b.data) CHECK(strcmp(b.data, "hello") == 0);
|
|
cf_buf_free(&b);
|
|
}
|
|
|
|
/* M2: a server response larger than CF_MAX_RESPONSE_BYTES must abort
|
|
* mid-transfer, not silently consume the heap. */
|
|
static void t_response_body_size_cap(void) {
|
|
TEST("response_body_size_cap");
|
|
reset_captures();
|
|
/* CF_MAX_RESPONSE_BYTES is 64 MiB. Stream 65 MiB so we cross the cap. */
|
|
long over_cap = (long)CF_MAX_RESPONSE_BYTES + (1L << 20);
|
|
enqueue_stream_response(200, over_cap);
|
|
|
|
cf_client_t *c = cf_client_new(make_base_url(), "tok");
|
|
cf_client_set_timeout_secs(c, 30);
|
|
cf_healthz_t h = {0};
|
|
cf_error_t err = {0};
|
|
cf_status_t s = cf_healthz(c, &h, &err);
|
|
CHECK(s == CF_ERR_TRANSPORT);
|
|
CHECK(err.message != NULL);
|
|
|
|
cf_healthz_free(&h);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
/* M3: with no CONNECTTIMEOUT set, libcurl waits ~300s. We set 10s,
|
|
* so a connect to 10.255.255.1 (RFC1918 unroutable) must fail well
|
|
* before the test-suite timeout (60s). */
|
|
static void t_connect_timeout(void) {
|
|
TEST("connect_timeout");
|
|
cf_client_t *c = cf_client_new("http://10.255.255.1:80", "tok");
|
|
/* Total timeout high so we know the connect-timeout is what trips. */
|
|
cf_client_set_timeout_secs(c, 30);
|
|
cf_healthz_t h = {0};
|
|
cf_error_t err = {0};
|
|
|
|
struct timeval t0, t1;
|
|
gettimeofday(&t0, NULL);
|
|
cf_status_t s = cf_healthz(c, &h, &err);
|
|
gettimeofday(&t1, NULL);
|
|
double elapsed = (t1.tv_sec - t0.tv_sec) +
|
|
(t1.tv_usec - t0.tv_usec) / 1e6;
|
|
|
|
CHECK(s == CF_ERR_TRANSPORT);
|
|
/* Allow 18s slack for slow CI; the libcurl default of 300s would
|
|
* blow the test-suite timeout long before this. */
|
|
CHECK(elapsed < 18.0);
|
|
|
|
cf_healthz_free(&h);
|
|
cf_error_free(&err);
|
|
cf_client_free(c);
|
|
}
|
|
|
|
/* M4: g_curl_init_count must survive concurrent cf_client_new /
|
|
* cf_client_free across threads without leaking, double-cleaning, or
|
|
* tearing down libcurl while another thread is mid-request. */
|
|
typedef struct {
|
|
int iters;
|
|
} init_thread_arg_t;
|
|
|
|
static void *init_worker(void *arg) {
|
|
init_thread_arg_t *a = (init_thread_arg_t *)arg;
|
|
for (int i = 0; i < a->iters; ++i) {
|
|
cf_client_t *c = cf_client_new("http://127.0.0.1:1", "tok");
|
|
if (c) {
|
|
cf_client_set_timeout_secs(c, 1);
|
|
cf_client_free(c);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void t_concurrent_client_init(void) {
|
|
TEST("concurrent_client_init");
|
|
enum { N = 4 };
|
|
pthread_t th[N];
|
|
init_thread_arg_t args[N];
|
|
for (int i = 0; i < N; ++i) {
|
|
args[i].iters = 50;
|
|
int rc = pthread_create(&th[i], NULL, init_worker, &args[i]);
|
|
CHECK(rc == 0);
|
|
}
|
|
for (int i = 0; i < N; ++i) {
|
|
pthread_join(th[i], NULL);
|
|
}
|
|
/* If we're still running with no crash and no SIGSEGV, the atomic
|
|
* refcount held. valgrind/asan will catch any heap corruption. */
|
|
CHECK(1);
|
|
}
|
|
|
|
/* CVE-2024-31755: prior to cJSON 1.7.18, calling cJSON_SetValuestring
|
|
* with a NULL `valuestring` argument on a string node would deref NULL
|
|
* inside strlen. The fix is a NULL check that returns NULL safely. */
|
|
static void t_cjson_bump(void) {
|
|
TEST("cjson_bump");
|
|
cJSON *node = cJSON_CreateString("seed");
|
|
CHECK(node != NULL);
|
|
if (node) {
|
|
/* Pre-fix: this call crashed. Post-fix: returns NULL. */
|
|
char *r = cJSON_SetValuestring(node, NULL);
|
|
CHECK(r == NULL);
|
|
/* The original valuestring must be untouched. */
|
|
CHECK(cJSON_IsString(node));
|
|
if (cJSON_IsString(node)) {
|
|
CHECK_STR_EQ(node->valuestring, "seed");
|
|
}
|
|
cJSON_Delete(node);
|
|
}
|
|
|
|
/* Belt-and-braces: parsing crafted JSON also returns sanely (NULL
|
|
* or a node), never crashes. */
|
|
const char *crafted = "{\"a\":[1,2,{\"b\":\"c\"}],\"d\":null}";
|
|
cJSON *parsed = cJSON_Parse(crafted);
|
|
CHECK(parsed != NULL);
|
|
if (parsed) cJSON_Delete(parsed);
|
|
|
|
/* And a malformed input: must return NULL, not crash. */
|
|
cJSON *bad = cJSON_Parse("{\"a\":");
|
|
CHECK(bad == NULL);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* main */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int main(void) {
|
|
/* Don't die on SIGPIPE if a peer hangs up. */
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
if (start_server() != 0) {
|
|
fprintf(stderr, "FATAL: could not start mock server\n");
|
|
return 2;
|
|
}
|
|
|
|
t_version();
|
|
t_client_lifecycle();
|
|
t_healthz_ok();
|
|
t_healthz_transport_fail();
|
|
t_run_success_object();
|
|
t_run_success_string();
|
|
t_run_files_passed();
|
|
t_run_502_envelope();
|
|
t_run_401_auth();
|
|
t_run_usage_errors();
|
|
t_upload_file();
|
|
t_admin_create_token();
|
|
t_admin_list_revoke();
|
|
t_admin_requires_token();
|
|
t_url_normalisation();
|
|
|
|
/* Audit-fix tests */
|
|
t_revoke_token_validates_name();
|
|
t_buf_append_overflow_guards();
|
|
t_response_body_size_cap();
|
|
t_connect_timeout();
|
|
t_concurrent_client_init();
|
|
t_cjson_bump();
|
|
|
|
stop_server();
|
|
|
|
fprintf(stderr, "\n%d tests, %d failures\n", g_tests, g_failures);
|
|
return g_failures == 0 ? 0 : 1;
|
|
}
|