;;; noise-sse-test.el --- Tests for SSE client -*- lexical-binding: t; -*- ;;; Commentary: ;; Tests for noise-sse.el SSE event parsing and connection management. ;;; Code: (require 'ert) (require 'noise-sse) (defvar noise-signal-cli-address) (defvar noise-account) (ert-deftest noise-sse--parse-event-basic () "Parse a basic SSE event with data field." (let ((event "data: {\"type\":\"message\",\"content\":\"hello\"}\n\n")) (let ((result (noise-sse--parse-event event))) (should (hash-table-p result)) (should (equal (gethash "type" result) "message")) (should (equal (gethash "content" result) "hello"))))) (ert-deftest noise-sse--parse-event-multiline () "Parse SSE event with multiline data." (let ((event "data: {\"type\":\"message\",\ndata: \"content\":\"hello\"}\n\n")) (let ((result (noise-sse--parse-event event))) (should (hash-table-p result)) (should (equal (gethash "type" result) "message"))))) (ert-deftest noise-sse--parse-event-empty () "Parse empty SSE event returns nil." (let ((event "\n\n")) (should (null (noise-sse--parse-event event))))) (ert-deftest noise-sse--initial-state () "SSE client starts in disconnected state." (let ((noise-sse--process nil) (noise-sse--reconnect-timer nil) (noise-sse--reconnect-delay 1)) (should (null noise-sse--process)) (should (null noise-sse--reconnect-timer)) (should (= 1 noise-sse--reconnect-delay)))) (ert-deftest noise-sse--reconnect-delay-backoff () "Reconnect delay doubles on each attempt." (let ((noise-sse--reconnect-delay 1)) (noise-sse--increase-reconnect-delay) (should (= 2 noise-sse--reconnect-delay)) (noise-sse--increase-reconnect-delay) (should (= 4 noise-sse--reconnect-delay)) (noise-sse--increase-reconnect-delay) (should (= 8 noise-sse--reconnect-delay)))) (ert-deftest noise-sse--reconnect-delay-max () "Reconnect delay caps at max value." (let ((noise-sse--reconnect-delay 32) (noise-sse-max-reconnect-delay 60)) (noise-sse--increase-reconnect-delay) (should (= 60 noise-sse--reconnect-delay)))) (ert-deftest noise-sse--filter-processes-complete-event () "Process filter parses and calls handler for complete events." (let ((processed-events '()) (noise-sse--buffer "") (noise-sse--http-buffer "") (noise-sse--chunk-buffer "") (noise-sse--headers-received t) (noise-sse--chunked-response-p nil)) (let ((noise-sse--event-handler (lambda (event) (push event processed-events)))) (noise-sse--filter nil "data: {\"type\":\"message\"}\n\n") (should (= 1 (length processed-events))) (should (hash-table-p (car processed-events)))))) (ert-deftest noise-sse--filter-buffers-incomplete-event () "Process filter buffers incomplete events until newline." (let ((processed-events '()) (noise-sse--buffer "") (noise-sse--http-buffer "") (noise-sse--chunk-buffer "") (noise-sse--headers-received t) (noise-sse--chunked-response-p nil)) (let ((noise-sse--event-handler (lambda (event) (push event processed-events)))) (noise-sse--filter nil "data: {\"type\":\"message\"") (should (= 0 (length processed-events))) (noise-sse--filter nil "}\n\n") (should (= 1 (length processed-events)))))) (ert-deftest noise-sse--filter-skips-http-headers () "HTTP response headers should not block the first SSE event." (let ((processed-events '()) (noise-sse--buffer "") (noise-sse--http-buffer "") (noise-sse--chunk-buffer "") (noise-sse--headers-received nil) (noise-sse--chunked-response-p nil)) (let ((noise-sse--event-handler (lambda (event) (push event processed-events)))) (noise-sse--filter nil (concat "HTTP/1.1 200 OK\r\n" "Content-Type: text/event-stream\r\n\r\n" "data: {\"type\":\"message\"}\n\n")) (should (= 1 (length processed-events))) (should (equal "message" (gethash "type" (car processed-events))))))) (ert-deftest noise-sse--filter-decodes-chunked-http-body () "Chunked HTTP SSE responses should be decoded before parsing events." (let ((processed-events '()) (noise-sse--buffer "") (noise-sse--http-buffer "") (noise-sse--chunk-buffer "") (noise-sse--headers-received nil) (noise-sse--chunked-response-p nil)) (let ((noise-sse--event-handler (lambda (event) (push event processed-events)))) (noise-sse--filter nil (concat "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "Content-Type: text/event-stream\r\n\r\n" "12\r\n" "data: {\"type\":\"msg" "\r\n" "4\r\n" "\"}\n\n" "\r\n" "0\r\n\r\n")) (should (= 1 (length processed-events))) (should (equal "msg" (gethash "type" (car processed-events))))))) (ert-deftest noise-sse--build-url () "Build SSE endpoint URL from base address." (let ((noise-signal-cli-address "http://localhost:8080") (noise-account "+15551234567")) (let ((url (noise-sse--build-url))) (should (string-match-p "/api/v1/events" url)) (should (string-match-p "account=%2B15551234567" url))))) (ert-deftest noise-sse--build-url-no-account () "Build SSE endpoint URL without account parameter." (let ((noise-signal-cli-address "http://localhost:8080") (noise-account nil)) (let ((url (noise-sse--build-url))) (should (string-match-p "/api/v1/events" url)) (should-not (string-match-p "account=" url))))) (ert-deftest noise-sse-connect-is-interactive () "noise-sse-connect is an interactive command." (should (commandp 'noise-sse-connect))) (ert-deftest noise-sse-disconnect-is-interactive () "noise-sse-disconnect is an interactive command." (should (commandp 'noise-sse-disconnect))) (ert-deftest noise-sse-disconnect-cleans-up () "noise-sse-disconnect cleans up process and timer." (let ((noise-sse--reconnect-timer (run-with-timer 1000 nil #'ignore))) (noise-sse-disconnect) (should (null noise-sse--process)) (should (null noise-sse--reconnect-timer)))) (provide 'noise-sse-test) ;;; noise-sse-test.el ends here