diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_sse_test.c	Tue Aug 04 16:49:11 2026 -0700
@@ -0,0 +1,211 @@
+#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;
+}