Skip to content

Commit df09ca2

Browse files
committed
Add SSE integration tests (Phase 5)
Two cross-phase integration tests in sse_integration_test.go: - TestSSEIntegration_RouteNoAuth: verifies GET /events returns 200 and text/event-stream without an Authorization header, using a pre-cancelled context so the handler exits immediately after writing headers. - TestSSEIntegration_MutationTriggersBroadcast: starts a real HTTP server, connects an SSE client, performs a POST /api/v1/beads mutation, and asserts the SSE stream delivers "data: update\n\n" within 1 second. Built with Raymond (Agent Orchestrator)
1 parent a676efe commit df09ca2

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package server
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
11+
"testing"
12+
"time"
13+
)
14+
15+
// TestSSEIntegration_RouteNoAuth verifies that GET /events is accessible without
16+
// an Authorization header and returns the correct SSE headers.
17+
func TestSSEIntegration_RouteNoAuth(t *testing.T) {
18+
srv := crudServer(t)
19+
20+
ctx, cancel := context.WithCancel(context.Background())
21+
cancel() // pre-cancel so the handler exits immediately after writing headers
22+
23+
req := httptest.NewRequest(http.MethodGet, "/events", nil).WithContext(ctx)
24+
w := httptest.NewRecorder()
25+
srv.Router.ServeHTTP(w, req)
26+
27+
if w.Code != http.StatusOK {
28+
t.Fatalf("expected 200, got %d", w.Code)
29+
}
30+
ct := w.Header().Get("Content-Type")
31+
if ct != "text/event-stream" {
32+
t.Errorf("expected Content-Type text/event-stream, got %q", ct)
33+
}
34+
}
35+
36+
// TestSSEIntegration_MutationTriggersBroadcast verifies the full path:
37+
// API mutation → broadcaster → SSE client receives "data: update\n\n".
38+
func TestSSEIntegration_MutationTriggersBroadcast(t *testing.T) {
39+
srv := crudServer(t)
40+
41+
ts := httptest.NewServer(srv.Router)
42+
defer ts.Close()
43+
44+
// Connect SSE client in a goroutine and collect received lines.
45+
received := make(chan string, 16)
46+
sseCtx, sseCancel := context.WithCancel(context.Background())
47+
defer sseCancel()
48+
49+
go func() {
50+
req, err := http.NewRequestWithContext(sseCtx, http.MethodGet, ts.URL+"/events", nil)
51+
if err != nil {
52+
return
53+
}
54+
resp, err := http.DefaultClient.Do(req)
55+
if err != nil {
56+
return
57+
}
58+
defer resp.Body.Close()
59+
60+
scanner := bufio.NewScanner(resp.Body)
61+
for scanner.Scan() {
62+
line := scanner.Text()
63+
received <- line
64+
}
65+
}()
66+
67+
// Give the SSE client time to connect and subscribe.
68+
time.Sleep(50 * time.Millisecond)
69+
70+
// Perform a write mutation via the API.
71+
body, _ := json.Marshal(map[string]any{"title": "integration test bead"})
72+
req, err := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/beads", bytes.NewReader(body))
73+
if err != nil {
74+
t.Fatalf("build request: %v", err)
75+
}
76+
req.Header.Set("Authorization", "Bearer "+testToken)
77+
req.Header.Set("Content-Type", "application/json")
78+
79+
resp, err := http.DefaultClient.Do(req)
80+
if err != nil {
81+
t.Fatalf("POST /api/v1/beads: %v", err)
82+
}
83+
resp.Body.Close()
84+
if resp.StatusCode != http.StatusCreated {
85+
t.Fatalf("expected 201, got %d", resp.StatusCode)
86+
}
87+
88+
// Wait for the debounced SSE event (debounce is 200ms; allow 1s total).
89+
deadline := time.After(1 * time.Second)
90+
for {
91+
select {
92+
case line := <-received:
93+
if strings.Contains(line, "data: update") {
94+
return // success
95+
}
96+
case <-deadline:
97+
t.Fatal("timed out waiting for SSE update event after mutation")
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)