calog/tests/testHttpd.c

171 lines
5.8 KiB
C

// testHttpd.c -- the polyglot HTTP server. A Lua context and a JavaScript context each stand up a
// server with route handlers written in their own language; the test drives real HTTP requests over
// a socket and checks the responses, a 404, a response map (custom status), and HOT RELOAD
// (re-registering a route on a live server swaps the handler).
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogHttpd.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define LUA_PORT 38900
#define JS_PORT 38901
#define PUMP_LIMIT 4000
static CalogT *calog = NULL;
static _Atomic int32_t readyCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
#define CHECK(cond, msg) checkImpl((cond), (msg), __LINE__)
static void checkImpl(bool condition, const char *message, int32_t line);
static bool httpGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap);
static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void pumpForReady(int32_t target);
static void checkImpl(bool condition, const char *message, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL testHttpd.c:%d %s\n", line, message);
}
}
// Minimal HTTP/1.1 client: connect, GET path, parse status code + body.
static bool httpGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap) {
struct sockaddr_in addr;
char request[256];
char response[8192];
char *bodyStart;
ssize_t total;
ssize_t got;
int fd;
int requestLen;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
return false;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
close(fd);
return false;
}
requestLen = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", path);
if (send(fd, request, (size_t)requestLen, 0) != requestLen) {
close(fd);
return false;
}
total = 0;
while ((got = recv(fd, response + total, sizeof(response) - 1 - (size_t)total, 0)) > 0) {
total += got;
if ((size_t)total >= sizeof(response) - 1) {
break;
}
}
close(fd);
if (total <= 0) {
return false;
}
response[total] = '\0';
*statusOut = 0;
if (strncmp(response, "HTTP/1.1 ", 9) == 0) {
*statusOut = atoi(response + 9);
}
bodyStart = strstr(response, "\r\n\r\n");
body[0] = '\0';
if (bodyStart != NULL) {
snprintf(body, bodyCap, "%s", bodyStart + 4);
}
return true;
}
static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&readyCount, 1);
calogValueNil(result);
return calogOkE;
}
static void pumpForReady(int32_t target) {
struct timespec ts = { 0, 500000 };
int32_t i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&readyCount) >= target) {
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *lua;
CalogContextT *js;
char body[8192];
int status;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogHttpdRegister(calog);
calogRegister(calog, "ready", nativeReady, NULL);
// Lua server: a plain-string route, a response-map route (custom status), and a 404 for the rest.
lua = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(lua,
"srv = httpdListen(38900)\n"
"httpdRoute(srv, 'GET', '/hi', function(req) return 'hello from lua path=' .. req.path end)\n"
"httpdRoute(srv, 'GET', '/made', function(req) return { status = 201, body = 'created' } end)\n"
"ready()");
pumpForReady(1);
// JavaScript server on another port -- a handler written in a different language.
js = calogContextOpen(calog, &calogJsEngine);
calogContextEval(js,
"var s = httpdListen(38901);\n"
"httpdRoute(s, 'GET', '/hi', function(req) { return 'hello from js'; });\n"
"ready();");
pumpForReady(2);
CHECK(httpGet(LUA_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strstr(body, "hello from lua") != NULL && strstr(body, "path=/hi") != NULL, "lua route serves + sees the request path");
CHECK(httpGet(JS_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strstr(body, "hello from js") != NULL, "js route serves on its own server");
CHECK(httpGet(LUA_PORT, "/made", &status, body, sizeof(body)) && status == 201 && strcmp(body, "created") == 0, "response map sets a custom status");
CHECK(httpGet(LUA_PORT, "/nope", &status, body, sizeof(body)) && status == 404, "an unmatched route is 404");
// Hot reload: re-register /hi on the LIVE lua server with a new handler.
atomic_store(&readyCount, 0);
calogContextEval(lua, "httpdRoute(srv, 'GET', '/hi', function(req) return 'reloaded' end); ready()");
pumpForReady(1);
CHECK(httpGet(LUA_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strcmp(body, "reloaded") == 0, "re-registering a route hot-swaps the handler");
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
return testsFailed == 0 ? 0 : 1;
}