Files
traefik-llmgateway/sse_test.go
T

412 lines
12 KiB
Go

package traefikllmgateway
import (
"bufio"
"bytes"
"errors"
"net/http"
"reflect"
"strings"
"testing"
)
// recordingResponseWriter is a minimal http.ResponseWriter that also
// implements http.Flusher, recording every write and flush so tests can
// pin sseWriter's exact wire output and its one-flush-per-event contract.
type recordingResponseWriter struct {
header http.Header
body bytes.Buffer
flushed int
}
func newRecordingResponseWriter() *recordingResponseWriter {
return &recordingResponseWriter{header: make(http.Header)}
}
func (w *recordingResponseWriter) Header() http.Header { return w.header }
func (w *recordingResponseWriter) Write(b []byte) (int, error) { return w.body.Write(b) }
func (w *recordingResponseWriter) WriteHeader(int) {}
func (w *recordingResponseWriter) Flush() { w.flushed++ }
// noFlushResponseWriter is an http.ResponseWriter that deliberately does
// NOT implement http.Flusher, proving sseWriter tolerates its absence.
type noFlushResponseWriter struct {
header http.Header
body bytes.Buffer
}
func newNoFlushResponseWriter() *noFlushResponseWriter {
return &noFlushResponseWriter{header: make(http.Header)}
}
func (w *noFlushResponseWriter) Header() http.Header { return w.header }
func (w *noFlushResponseWriter) Write(b []byte) (int, error) { return w.body.Write(b) }
func (w *noFlushResponseWriter) WriteHeader(int) {}
// recordingFlusher is a bare http.Flusher used to pin flushWriter's
// flush-after-every-write contract independent of any io.Writer.
type recordingFlusher struct{ flushed int }
func (f *recordingFlusher) Flush() { f.flushed++ }
// TestReadSSE_Fixture parses a fixture mixing a comment line, CRLF and LF
// line endings, and a multi-line data field, and pins the exact two events
// it must produce: multi-line data joins with "\n", and the field order in
// the stream does not matter to the result.
func TestReadSSE_Fixture(t *testing.T) {
raw := ": keep-alive\r\n" +
"event: message\r\n" +
"data: line one\r\n" +
"data: line two\r\n" +
"\r\n" +
"data: second event\n" +
"\n"
var got []sseEvent
err := readSSE(strings.NewReader(raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
want := []sseEvent{
{event: "message", data: []byte("line one\nline two")},
{event: "", data: []byte("second event")},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestReadSSE_IDAndRetryIgnored proves "id:" and "retry:" fields are
// recognized and dropped: they neither leak into the dispatched event nor
// trigger a dispatch of their own.
func TestReadSSE_IDAndRetryIgnored(t *testing.T) {
raw := "id: 42\n" +
"retry: 3000\n" +
"data: hello\n" +
"\n"
var got []sseEvent
err := readSSE(strings.NewReader(raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
want := []sseEvent{{event: "", data: []byte("hello")}}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestReadSSE_FinalEventFlushedAtEOF proves an event with no trailing
// blank line before EOF still gets dispatched once, not silently dropped.
func TestReadSSE_FinalEventFlushedAtEOF(t *testing.T) {
raw := "event: partial\ndata: leftover"
var got []sseEvent
err := readSSE(strings.NewReader(raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
want := []sseEvent{{event: "partial", data: []byte("leftover")}}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestReadSSE_StopsOnFnError proves readSSE returns fn's error verbatim
// (checkable with errors.Is) and stops after the first event instead of
// continuing to parse and dispatch the rest of the stream.
func TestReadSSE_StopsOnFnError(t *testing.T) {
raw := "data: first\n\ndata: second\n\n"
sentinel := errors.New("boom")
var calls int
err := readSSE(strings.NewReader(raw), func(sseEvent) error {
calls++
return sentinel
})
if !errors.Is(err, sentinel) {
t.Fatalf("err = %v, want sentinel %v", err, sentinel)
}
if calls != 1 {
t.Errorf("calls = %d, want 1 (must stop after fn's first error)", calls)
}
}
// TestReadSSE_LineTooLong proves a single field line beyond
// sseScannerMaxLineSize surfaces the scanner's own error instead of
// growing the buffer without bound.
func TestReadSSE_LineTooLong(t *testing.T) {
huge := strings.Repeat("a", sseScannerMaxLineSize+10)
raw := "data: " + huge + "\n\n"
err := readSSE(strings.NewReader(raw), func(sseEvent) error { return nil })
if !errors.Is(err, bufio.ErrTooLong) {
t.Fatalf("err = %v, want bufio.ErrTooLong", err)
}
}
// TestReadSSE_BlankLinesWithoutFieldsDoNotDispatch proves blank lines and
// comment-only content, with no "event:"/"data:" field accumulated,
// produce zero dispatched events rather than empty ones.
func TestReadSSE_BlankLinesWithoutFieldsDoNotDispatch(t *testing.T) {
raw := "\n\n: just a comment\n\n"
var calls int
err := readSSE(strings.NewReader(raw), func(sseEvent) error {
calls++
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
if calls != 0 {
t.Errorf("calls = %d, want 0", calls)
}
}
// TestReadSSE_EventFieldAloneDoesNotDispatch proves an "event:" field with
// no "data:" line does not produce a data-less event on the blank line
// that follows it — matching the EventSource spec, which discards an
// event whose data buffer is empty even when the event type was set.
func TestReadSSE_EventFieldAloneDoesNotDispatch(t *testing.T) {
raw := "event: ping\n\ndata: real\n\n"
var got []sseEvent
err := readSSE(strings.NewReader(raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
want := []sseEvent{{event: "", data: []byte("real")}}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestReadSSE_EmptyDataFieldDoesNotDispatch proves a "data:" line with no
// value produces no event: the joined data buffer is empty, and the
// specification discards a dispatch in that case. "data: {}" still
// dispatches, since "{}" is a non-empty payload.
func TestReadSSE_EmptyDataFieldDoesNotDispatch(t *testing.T) {
cases := []struct {
name string
raw string
want []sseEvent
}{
{name: "empty data value", raw: "data:\n\n", want: nil},
{name: "empty JSON object payload", raw: "data: {}\n\n", want: []sseEvent{{event: "", data: []byte("{}")}}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var got []sseEvent
err := readSSE(strings.NewReader(c.raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
if !reflect.DeepEqual(got, c.want) {
t.Errorf("got %#v, want %#v", got, c.want)
}
})
}
}
// TestReadSSE_StripsLeadingBOM proves a UTF-8 byte-order mark on the
// stream's first line does not attach to the first field name and drop
// the first event.
func TestReadSSE_StripsLeadingBOM(t *testing.T) {
raw := string(rune(0xFEFF)) + "data: first\n\ndata: second\n\n"
var got []sseEvent
err := readSSE(strings.NewReader(raw), func(ev sseEvent) error {
got = append(got, ev)
return nil
})
if err != nil {
t.Fatalf("readSSE: %v", err)
}
want := []sseEvent{
{event: "", data: []byte("first")},
{event: "", data: []byte("second")},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestNewSSEWriter_SetsHeaders proves newSSEWriter sets the three SSE
// response headers on construction, before any body write.
func TestNewSSEWriter_SetsHeaders(t *testing.T) {
w := newRecordingResponseWriter()
newSSEWriter(w)
cases := map[string]string{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
}
for header, want := range cases {
if got := w.Header().Get(header); got != want {
t.Errorf("header %q = %q, want %q", header, got, want)
}
}
if w.body.Len() != 0 {
t.Errorf("body = %q, want empty (headers must be set before any write)", w.body.String())
}
}
// TestSSEWriter_WriteData pins the exact wire bytes writeData produces —
// "data: " + payload + "\n\n" — and proves it flushes once per call.
func TestSSEWriter_WriteData(t *testing.T) {
w := newRecordingResponseWriter()
s := newSSEWriter(w)
if err := s.writeData([]byte(`{"a":1}`)); err != nil {
t.Fatalf("writeData: %v", err)
}
if err := s.writeData([]byte("second")); err != nil {
t.Fatalf("writeData: %v", err)
}
want := "data: {\"a\":1}\n\ndata: second\n\n"
if got := w.body.String(); got != want {
t.Errorf("body = %q, want %q", got, want)
}
if w.flushed != 2 {
t.Errorf("flushed = %d, want 2 (one Flush per writeData call)", w.flushed)
}
}
// TestSSEWriter_WriteDone pins the "[DONE]" sentinel's exact wire bytes and
// proves it flushes.
func TestSSEWriter_WriteDone(t *testing.T) {
w := newRecordingResponseWriter()
s := newSSEWriter(w)
s.writeDone()
want := "data: [DONE]\n\n"
if got := w.body.String(); got != want {
t.Errorf("body = %q, want %q", got, want)
}
if w.flushed != 1 {
t.Errorf("flushed = %d, want 1", w.flushed)
}
}
// TestSSEWriter_ToleratesMissingFlusher proves writeData/writeDone still
// write the correct bytes, without panicking, when the underlying
// http.ResponseWriter does not implement http.Flusher.
func TestSSEWriter_ToleratesMissingFlusher(t *testing.T) {
w := newNoFlushResponseWriter()
s := newSSEWriter(w)
if err := s.writeData([]byte("x")); err != nil {
t.Fatalf("writeData: %v", err)
}
s.writeDone()
want := "data: x\n\ndata: [DONE]\n\n"
if got := w.body.String(); got != want {
t.Errorf("body = %q, want %q", got, want)
}
}
// TestFlushWriter_WritesAndFlushes proves Write delegates to the wrapped
// io.Writer and calls Flush once per Write when f is non-nil.
func TestFlushWriter_WritesAndFlushes(t *testing.T) {
var buf bytes.Buffer
fl := &recordingFlusher{}
fw := &flushWriter{w: &buf, f: fl}
n, err := fw.Write([]byte("chunk"))
if err != nil {
t.Fatalf("Write: %v", err)
}
if n != len("chunk") {
t.Errorf("n = %d, want %d", n, len("chunk"))
}
if _, err := fw.Write([]byte("more")); err != nil {
t.Fatalf("Write: %v", err)
}
if got := buf.String(); got != "chunkmore" {
t.Errorf("buf = %q, want %q", got, "chunkmore")
}
if fl.flushed != 2 {
t.Errorf("flushed = %d, want 2", fl.flushed)
}
}
// TestFlushWriter_NilFlusher proves Write still delegates correctly, and
// does not panic, when f is nil.
func TestFlushWriter_NilFlusher(t *testing.T) {
var buf bytes.Buffer
fw := &flushWriter{w: &buf}
n, err := fw.Write([]byte("ok"))
if err != nil {
t.Fatalf("Write: %v", err)
}
if n != len("ok") {
t.Errorf("n = %d, want %d", n, len("ok"))
}
if got := buf.String(); got != "ok" {
t.Errorf("buf = %q, want %q", got, "ok")
}
}
// TestNewFlushWriter_AssertsFlusherOnce proves newFlushWriter captures
// the http.Flusher assertion at construction: a wrapped writer that
// implements http.Flusher gets flushed on every Write, and one that does
// not still writes correctly, with no panic.
func TestNewFlushWriter_AssertsFlusherOnce(t *testing.T) {
t.Run("flusher present", func(t *testing.T) {
w := newRecordingResponseWriter()
fw := newFlushWriter(w)
if _, err := fw.Write([]byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
if w.flushed != 1 {
t.Errorf("flushed = %d, want 1", w.flushed)
}
})
t.Run("flusher absent", func(t *testing.T) {
var buf bytes.Buffer
fw := newFlushWriter(&buf)
n, err := fw.Write([]byte("ok"))
if err != nil {
t.Fatalf("Write: %v", err)
}
if n != len("ok") {
t.Errorf("n = %d, want %d", n, len("ok"))
}
if got := buf.String(); got != "ok" {
t.Errorf("buf = %q, want %q", got, "ok")
}
})
}