Mercurial
view seobeo/tests/seobeo_sse_test.c @ 270:358dd5950985
sync production config on every deploy
Always install the ignored repository .config into /etc/mrjunejune before promoting the new bundle.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 13:16:47 -0700 |
| parents | 04fee26ecce0 |
| children |
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> /* Counter incremented by the detach callback. */ static _Atomic int g_detach_count = 0; static Seobeo_SSE_Stream *g_detach_stream = NULL; static void test_detach_cb(Seobeo_SSE_Stream *p_stream, void *ctx) { (void)ctx; g_detach_stream = p_stream; atomic_fetch_add(&g_detach_count, 1); } 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 post_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]); /* * Detach callback test: verify callback fires after handle detach and * that the callback is called outside g_sse_mutex (no deadlock). */ { int cbs[2]; assert(socketpair(AF_UNIX, SOCK_STREAM, 0, cbs) == 0); uint8 cb_write_buf[4096] = {0}; Seobeo_Handle cb_handle; initialize_handle(&cb_handle, cbs[0], cb_write_buf, sizeof(cb_write_buf)); Seobeo_SSE_Stream *p_cb_stream = Seobeo_SSE_Server_Attach(&cb_handle, "/cb"); assert(p_cb_stream); assert(Seobeo_SSE_Retain(p_cb_stream)); /* extra reference */ /* Register detach callback. */ atomic_store(&g_detach_count, 0); g_detach_stream = NULL; Seobeo_SSE_Set_Detach_Callback(p_cb_stream, test_detach_cb, NULL); /* Detach: fires callback outside the mutex. */ Seobeo_SSE_Server_Detach_Handle(&cb_handle); assert(atomic_load(&g_detach_count) == 1); assert(g_detach_stream == p_cb_stream); assert(!Seobeo_SSE_Is_Open(p_cb_stream)); /* Second detach: no stream found, callback NOT called again. */ Seobeo_SSE_Server_Detach_Handle(&cb_handle); assert(atomic_load(&g_detach_count) == 1); /* Clear the callback and verify it isn't called on a subsequent release. */ Seobeo_SSE_Set_Detach_Callback(p_cb_stream, NULL, NULL); Seobeo_SSE_Release(p_cb_stream); /* release extra reference */ Seobeo_SSE_Server_Destroy(); close(cbs[0]); close(cbs[1]); printf(" detach callback fires once, outside mutex PASS\n"); } Seobeo_Router_Init(); Seobeo_Router_Register_SSE("/events/:topic", route_handler); Seobeo_Router_Register_SSE("/events/:topic/fixed", route_handler); Seobeo_Router_Register_SSE_Method( "POST", "/events/:topic", post_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); Seobeo_Request_Entry *p_post_request = NULL; Dowa_HashMap_Push_Arena( p_post_request, "Body", "deploy=release", p_arena); assert(Seobeo_Router_Find_SSE_Handler( "POST", "/events/builds", &p_post_request, p_arena) == post_route_handler); p_topic = Dowa_HashMap_Get_Ptr(p_post_request, ":topic"); Seobeo_Request_Entry *p_body = Dowa_HashMap_Get_Ptr( p_post_request, "Body"); assert(p_topic && strcmp(p_topic->value, "builds") == 0); assert(p_body && strcmp(p_body->value, "deploy=release") == 0); Seobeo_Request_Entry *p_wrong_method = NULL; assert(Seobeo_Router_Find_SSE_Handler( "PUT", "/events/builds", &p_wrong_method, p_arena) == NULL); assert(Dowa_HashMap_Get_Ptr(p_wrong_method, ":topic") == 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; }