view seobeo/tests/seobeo_sse_server_test.c @ 257:609d3c6aff4e

[seobeo] Add persistent SSE streams Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 16:49:11 -0700
parents
children 1f9877b637e9
line wrap: on
line source

#include "seobeo/seobeo.h"

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

static int reserve_port(void)
{
  int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
  assert(socket_fd >= 0);
  struct sockaddr_in address = {
    .sin_family = AF_INET,
    .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
    .sin_port = 0,
  };
  assert(bind(
      socket_fd,
      (struct sockaddr *)&address,
      sizeof(address)) == 0);
  socklen_t length = sizeof(address);
  assert(getsockname(
      socket_fd,
      (struct sockaddr *)&address,
      &length) == 0);
  int port = ntohs(address.sin_port);
  close(socket_fd);
  return port;
}

static int connect_to(int port)
{
  int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (socket_fd < 0)
    return -1;
  struct timeval timeout = {
    .tv_sec = 3,
  };
  setsockopt(
      socket_fd,
      SOL_SOCKET,
      SO_RCVTIMEO,
      &timeout,
      sizeof(timeout));
  struct sockaddr_in address = {
    .sin_family = AF_INET,
    .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
    .sin_port = htons((uint16)port),
  };
  if (connect(
      socket_fd,
      (struct sockaddr *)&address,
      sizeof(address)) != 0)
  {
    close(socket_fd);
    return -1;
  }
  return socket_fd;
}

static void send_request(int socket_fd, const char *path)
{
  char request[512];
  int length = snprintf(
      request,
      sizeof(request),
      "GET %s HTTP/1.1\r\n"
      "Host: 127.0.0.1\r\n"
      "Connection: keep-alive\r\n"
      "\r\n",
      path);
  assert(length > 0 && (size_t)length < sizeof(request));
  assert(send(socket_fd, request, (size_t)length, 0) == length);
}

static size_t receive_until(
    int socket_fd,
    char *buffer,
    size_t capacity,
    const char *needle)
{
  size_t used = 0;
  while (used + 1 < capacity)
  {
    ssize_t amount = recv(
        socket_fd,
        buffer + used,
        capacity - used - 1,
        0);
    assert(amount > 0);
    used += (size_t)amount;
    buffer[used] = '\0';
    if (strstr(buffer, needle))
      return used;
  }
  assert(FALSE && "response exceeded test buffer");
  return 0;
}

static pid_t start_server(
    const char *binary,
    const char *port)
{
  pid_t pid = fork();
  assert(pid >= 0);
  if (pid == 0)
  {
    execl(binary, binary, port, NULL);
    _exit(127);
  }

  int numeric_port = atoi(port);
  for (int attempt = 0; attempt < 100; attempt++)
  {
    int socket_fd = connect_to(numeric_port);
    if (socket_fd >= 0)
    {
      close(socket_fd);
      return pid;
    }
    usleep(20000);
  }
  kill(pid, SIGTERM);
  waitpid(pid, NULL, 0);
  assert(FALSE && "SSE test server did not start");
  return -1;
}

int main(int argc, char **argv)
{
  assert(argc == 2);
  int port = reserve_port();
  char port_text[16];
  snprintf(port_text, sizeof(port_text), "%d", port);
  pid_t server = start_server(argv[1], port_text);

  int event_socket = connect_to(port);
  assert(event_socket >= 0);
  send_request(event_socket, "/events");
  char events[4096] = {0};
  receive_until(
      event_socket,
      events,
      sizeof(events),
      "data: later\n\n");
  assert(strstr(events, "HTTP/1.1 200 OK\r\n"));
  assert(strstr(events, "Content-Type: text/event-stream\r\n"));
  assert(strstr(events, "data: connected\n\n"));

  int health_socket = connect_to(port);
  assert(health_socket >= 0);
  send_request(health_socket, "/health");
  assert(shutdown(health_socket, SHUT_WR) == 0);
  char health[1024] = {0};
  receive_until(health_socket, health, sizeof(health), "\r\n\r\nok");
  assert(strstr(health, "HTTP/1.1 200 OK\r\n"));

  close(health_socket);
  close(event_socket);
  kill(server, SIGTERM);
  waitpid(server, NULL, 0);
  printf("Seobeo SSE server integration test passed\n");
  return 0;
}