view seobeo/tests/seobeo_sse_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 <fcntl.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

static void read_expected(int socket_fd, const char *expected)
{
  size_t expected_length = strlen(expected);
  size_t received = 0;
  char buffer[8192] = {0};
  assert(expected_length < sizeof(buffer));

  while (received < expected_length)
  {
    ssize_t amount = recv(
        socket_fd,
        buffer + received,
        expected_length - received,
        0);
    assert(amount > 0);
    received += (size_t)amount;
  }
  assert(received == expected_length);
  assert(memcmp(buffer, expected, expected_length) == 0);
}

static void route_handler(
    Seobeo_SSE_Stream *p_stream,
    Seobeo_Request_Entry *p_request,
    Dowa_Arena *p_arena)
{
  (void)p_stream;
  (void)p_request;
  (void)p_arena;
}

static void initialize_handle(
    Seobeo_Handle *p_handle,
    int socket_fd,
    uint8 *write_buffer,
    uint32 write_capacity)
{
  memset(p_handle, 0, sizeof(*p_handle));
  p_handle->socket = socket_fd;
  p_handle->connected = TRUE;
  p_handle->write_buffer = write_buffer;
  p_handle->write_buffer_capacity = write_capacity;
  atomic_init(&p_handle->destroyed, FALSE);
}

int main(void)
{
  const char *headers =
      "HTTP/1.1 200 OK\r\n"
      "Content-Type: text/event-stream\r\n"
      "Cache-Control: no-cache\r\n"
      "Connection: keep-alive\r\n"
      "X-Accel-Buffering: no\r\n"
      "\r\n";
  int sockets[2];
  assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);

  uint8 write_buffer[4096] = {0};
  Seobeo_Handle handle;
  initialize_handle(
      &handle,
      sockets[0],
      write_buffer,
      (uint32)sizeof(write_buffer));
  Seobeo_SSE_Stream stream = {0};
  assert(Seobeo_SSE_Start(&stream, &handle) == 0);
  assert(Seobeo_SSE_Is_Open(&stream));
  read_expected(sockets[1], headers);

  Seobeo_SSE_Event event = {
    .event = "deployment",
    .id = "42",
    .data = "started\r\nrunning",
    .retry_ms = 1500,
  };
  assert(Seobeo_SSE_Send(&stream, &event) == 0);
  read_expected(
      sockets[1],
      "id: 42\n"
      "event: deployment\n"
      "retry: 1500\n"
      "data: started\n"
      "data: running\n"
      "\n");

  assert(Seobeo_SSE_Send_Comment(
      &stream,
      "heartbeat\nstill here") == 0);
  read_expected(
      sockets[1],
      ": heartbeat\n"
      ": still here\n"
      "\n");

  int flags = fcntl(sockets[0], F_GETFL, 0);
  assert(flags >= 0);
  assert(fcntl(sockets[0], F_SETFL, flags | O_NONBLOCK) == 0);
  char filler[4096];
  memset(filler, 'z', sizeof(filler));
  while (send(sockets[0], filler, sizeof(filler), 0) > 0)
  {
  }
  assert(errno == EAGAIN || errno == EWOULDBLOCK);
  memcpy(handle.write_buffer, "older", 5);
  handle.write_buffer_len = 5;
  assert(Seobeo_SSE_Send_Data(&stream, "queued") == 1);
  while (recv(sockets[1], filler, sizeof(filler), MSG_DONTWAIT) > 0)
  {
  }
  assert(errno == EAGAIN || errno == EWOULDBLOCK);
  assert(Seobeo_SSE_Flush(&stream) == 0);
  read_expected(sockets[1], "olderdata: queued\n\n");

  event.event = "bad\nevent";
  assert(Seobeo_SSE_Send(&stream, &event) == SEOBEO_SSE_INVALID_EVENT);
  event.event = NULL;
  event.id = "bad\rid";
  assert(Seobeo_SSE_Send(&stream, &event) == SEOBEO_SSE_INVALID_EVENT);

  char oversized[4097];
  memset(oversized, 'x', sizeof(oversized) - 1);
  oversized[sizeof(oversized) - 1] = '\0';
  assert(Seobeo_SSE_Send_Data(
      &stream,
      oversized) == SEOBEO_SSE_EVENT_TOO_LARGE);

  Seobeo_SSE_Close(&stream);
  assert(!Seobeo_SSE_Is_Open(&stream));
  assert(Seobeo_SSE_Send_Data(&stream, "closed") < 0);
  close(sockets[0]);
  close(sockets[1]);

  assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);
  initialize_handle(
      &handle,
      sockets[0],
      write_buffer,
      (uint32)sizeof(write_buffer));
  memset(&stream, 0, sizeof(stream));
  assert(Seobeo_SSE_Start(&stream, &handle) == 0);
  read_expected(sockets[1], headers);
  close(sockets[1]);
  assert(Seobeo_SSE_Send_Data(&stream, "disconnect") < 0);
  close(sockets[0]);

  assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);
  initialize_handle(
      &handle,
      sockets[0],
      write_buffer,
      (uint32)sizeof(write_buffer));
  Seobeo_SSE_Stream *p_managed = Seobeo_SSE_Server_Attach(
      &handle,
      "/events/builds");
  assert(p_managed);
  assert(handle.is_sse);
  assert(Seobeo_SSE_Retain(p_managed));
  read_expected(sockets[1], headers);
  assert(Seobeo_SSE_Send_Data(p_managed, "after-handler") == 0);
  read_expected(sockets[1], "data: after-handler\n\n");
  Seobeo_SSE_Server_Detach_Handle(&handle);
  assert(!handle.is_sse);
  assert(!Seobeo_SSE_Is_Open(p_managed));
  Seobeo_SSE_Release(p_managed);
  Seobeo_SSE_Server_Destroy();
  close(sockets[0]);
  close(sockets[1]);

  Seobeo_Router_Init();
  Seobeo_Router_Register_SSE("/events/:topic", route_handler);
  Seobeo_Router_Register_SSE("/events/:topic/fixed", route_handler);
  Dowa_Arena *p_arena = Dowa_Arena_Create(4096);
  Seobeo_Request_Entry *p_request = NULL;
  Seobeo_SSE_Handler handler = Seobeo_Router_Find_SSE_Handler(
      "GET",
      "/events/builds",
      &p_request,
      p_arena);
  assert(handler == route_handler);
  Seobeo_Request_Entry *p_topic = Dowa_HashMap_Get_Ptr(
      p_request,
      ":topic");
  assert(p_topic && strcmp(p_topic->value, "builds") == 0);
  assert(Seobeo_Router_Find_SSE_Handler(
      "POST",
      "/events/builds",
      &p_request,
      p_arena) == NULL);
  Seobeo_Request_Entry *p_unmatched = NULL;
  assert(Seobeo_Router_Find_SSE_Handler(
      "GET",
      "/events/builds/other",
      &p_unmatched,
      p_arena) == NULL);
  assert(Dowa_HashMap_Get_Ptr(p_unmatched, ":topic") == NULL);
  Dowa_Arena_Free(p_arena);
  Seobeo_Router_Destroy();

  printf("Seobeo SSE tests passed\n");
  return 0;
}