mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 15:47:38 +08:00
Address PR review feedback: - Capture w := c.Writer before goroutine start, use w (not c.Writer) inside the goroutine to avoid holding *Context reference - Move channel send into select alongside context cancellation in all examples and tests, preventing goroutine leak when client disconnects while blocked on unbuffered send
142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
package touka
|
|
|
|
import (
|
|
"context"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestEventStreamChanBlocksHandler verifies that EventStreamChan blocks until
|
|
// the event channel is closed.
|
|
func TestEventStreamChanBlocksHandler(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/sse", nil)
|
|
c, _ := CreateTestContextWithRequest(rr, req)
|
|
|
|
handlerReturned := make(chan struct{})
|
|
eventChan := make(chan Event)
|
|
|
|
// Start producer goroutine before EventStreamChan blocks
|
|
go func() {
|
|
defer close(eventChan)
|
|
time.Sleep(30 * time.Millisecond)
|
|
eventChan <- Event{Data: "hello"}
|
|
time.Sleep(30 * time.Millisecond)
|
|
}()
|
|
|
|
go func() {
|
|
c.EventStreamChan(eventChan)
|
|
close(handlerReturned)
|
|
}()
|
|
|
|
// Wait for goroutine to start
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Handler should NOT have returned (eventChan not closed)
|
|
select {
|
|
case <-handlerReturned:
|
|
t.Fatal("Handler returned before eventChan was closed - EventStreamChan is not blocking")
|
|
case <-time.After(40 * time.Millisecond):
|
|
// good, still blocking
|
|
}
|
|
|
|
// Wait for producer to finish (30+30ms + margin)
|
|
select {
|
|
case <-handlerReturned:
|
|
// good, handler returned
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("Handler did not return after eventChan was closed")
|
|
}
|
|
}
|
|
|
|
// TestEventStreamChanUnblocksOnClientDisconnect verifies the handler returns
|
|
// when the request context is cancelled, even if eventChan is never closed.
|
|
func TestEventStreamChanUnblocksOnClientDisconnect(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/sse", nil).WithContext(ctx)
|
|
c, _ := CreateTestContextWithRequest(rr, req)
|
|
|
|
eventChan := make(chan Event)
|
|
handlerReturned := make(chan struct{})
|
|
|
|
// Producer never closes eventChan
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case eventChan <- Event{Data: "tick"}:
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
c.EventStreamChan(eventChan)
|
|
close(handlerReturned)
|
|
}()
|
|
|
|
// Handler should NOT have returned
|
|
select {
|
|
case <-handlerReturned:
|
|
t.Fatal("Handler returned before stream ended")
|
|
case <-time.After(60 * time.Millisecond):
|
|
// good, still blocked
|
|
}
|
|
|
|
// Cancel context to simulate client disconnect
|
|
cancel()
|
|
|
|
select {
|
|
case <-handlerReturned:
|
|
// good
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("Handler did not return after client disconnect")
|
|
}
|
|
}
|
|
|
|
// TestEventStreamChanWritesEvents verifies the SSE event format is correct.
|
|
func TestEventStreamChanWritesEvents(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/sse", nil)
|
|
c, _ := CreateTestContextWithRequest(rr, req)
|
|
|
|
eventChan := make(chan Event)
|
|
|
|
go func() {
|
|
defer close(eventChan)
|
|
eventChan <- Event{Id: "1", Event: "tick", Data: "hello\nworld"}
|
|
eventChan <- Event{Id: "2", Data: "second"}
|
|
}()
|
|
|
|
c.EventStreamChan(eventChan)
|
|
|
|
body := rr.Body.String()
|
|
|
|
ct := rr.Header().Get("Content-Type")
|
|
if !strings.Contains(ct, "text/event-stream") {
|
|
t.Fatalf("expected text/event-stream content type, got %q", ct)
|
|
}
|
|
|
|
if !strings.Contains(body, "id: 1") {
|
|
t.Fatal("missing id field in first event")
|
|
}
|
|
if !strings.Contains(body, "event: tick") {
|
|
t.Fatal("missing event field in first event")
|
|
}
|
|
if !strings.Contains(body, "data: hello") {
|
|
t.Fatal("missing data line 1 in first event")
|
|
}
|
|
if !strings.Contains(body, "data: world") {
|
|
t.Fatal("missing data line 2 in first event")
|
|
}
|
|
if !strings.Contains(body, "id: 2") {
|
|
t.Fatal("missing id field in second event")
|
|
}
|
|
if !strings.Contains(body, "data: second") {
|
|
t.Fatal("missing data in second event")
|
|
}
|
|
}
|