Skip to content

Commit b6a1eef

Browse files
committed
missing files
1 parent aced381 commit b6a1eef

6 files changed

Lines changed: 249 additions & 0 deletions

File tree

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package server
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"strings"
12+
"testing"
13+
"time"
14+
)
15+
16+
// TestOrgSampleStreamIntegration tests the org sample stream endpoint end-to-end.
17+
// This test verifies that:
18+
// - SSE events are received
19+
// - Progress updates are sent for each PR being fetched
20+
// - A final "done" event is received
21+
// - The stream doesn't timeout during long operations
22+
func TestOrgSampleStreamIntegration(t *testing.T) {
23+
// Skip if no GitHub token is available
24+
token := os.Getenv("GITHUB_TOKEN")
25+
if token == "" {
26+
t.Skip("GITHUB_TOKEN not set, skipping integration test")
27+
}
28+
29+
// Create server
30+
s := New()
31+
s.SetRateLimit(1000, 1000) // High rate limit for testing
32+
33+
// Create request
34+
reqBody := OrgSampleRequest{
35+
Org: "codeGROOVE-dev",
36+
SampleSize: 25,
37+
Days: 90,
38+
}
39+
body, err := json.Marshal(reqBody)
40+
if err != nil {
41+
t.Fatalf("Failed to marshal request: %v", err)
42+
}
43+
44+
req := httptest.NewRequest(http.MethodPost, "/v1/calculate/org/stream", bytes.NewReader(body))
45+
req.Header.Set("Content-Type", "application/json")
46+
req.Header.Set("Authorization", "Bearer "+token)
47+
48+
// Use a recorder that supports flushing
49+
w := httptest.NewRecorder()
50+
51+
// Run the handler in a goroutine since it's a streaming endpoint
52+
done := make(chan bool)
53+
go func() {
54+
s.handleOrgSampleStream(w, req)
55+
close(done)
56+
}()
57+
58+
// Wait for the handler to complete or timeout
59+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
60+
defer cancel()
61+
62+
select {
63+
case <-done:
64+
// Handler completed successfully
65+
case <-ctx.Done():
66+
t.Fatal("Handler timed out after 5 minutes")
67+
}
68+
69+
// Parse SSE events from response
70+
scanner := bufio.NewScanner(strings.NewReader(w.Body.String()))
71+
var events []ProgressUpdate
72+
var currentData string
73+
74+
for scanner.Scan() {
75+
line := scanner.Text()
76+
77+
if strings.HasPrefix(line, "data: ") {
78+
currentData = strings.TrimPrefix(line, "data: ")
79+
} else if line == "" && currentData != "" {
80+
// Empty line marks end of SSE event
81+
var event ProgressUpdate
82+
if err := json.Unmarshal([]byte(currentData), &event); err != nil {
83+
t.Logf("Warning: Failed to parse SSE event: %v (data: %s)", err, currentData)
84+
} else {
85+
events = append(events, event)
86+
}
87+
currentData = ""
88+
}
89+
}
90+
91+
if err := scanner.Err(); err != nil {
92+
t.Fatalf("Error reading response: %v", err)
93+
}
94+
95+
// Verify we received events
96+
if len(events) == 0 {
97+
t.Fatal("No SSE events received")
98+
}
99+
100+
t.Logf("Received %d SSE events", len(events))
101+
102+
// Track event types
103+
eventTypes := make(map[string]int)
104+
var fetchingEvents, processingEvents, completeEvents, errorEvents int
105+
var finalDone bool
106+
107+
for i, event := range events {
108+
eventTypes[event.Type]++
109+
110+
switch event.Type {
111+
case "fetching":
112+
fetchingEvents++
113+
t.Logf("Event %d: fetching PR #%d (%s)", i+1, event.PR, event.Progress)
114+
115+
case "processing":
116+
processingEvents++
117+
t.Logf("Event %d: processing PR #%d (%s)", i+1, event.PR, event.Progress)
118+
119+
case "complete":
120+
completeEvents++
121+
t.Logf("Event %d: complete PR #%d (%s)", i+1, event.PR, event.Progress)
122+
123+
case "error":
124+
errorEvents++
125+
t.Logf("Event %d: error - %s (PR #%d)", i+1, event.Error, event.PR)
126+
127+
case "done":
128+
finalDone = true
129+
if event.Result == nil {
130+
t.Error("Final 'done' event has nil Result")
131+
} else {
132+
t.Logf("Event %d: done - Total cost: $%.2f (from %d sampled PRs)",
133+
i+1, event.Result.TotalCost, event.Result.SampledPRs)
134+
}
135+
136+
default:
137+
t.Logf("Event %d: unknown type '%s'", i+1, event.Type)
138+
}
139+
}
140+
141+
// Assertions
142+
t.Logf("Event summary: %d fetching, %d processing, %d complete, %d error, final done: %v",
143+
fetchingEvents, processingEvents, completeEvents, errorEvents, finalDone)
144+
145+
// We should receive at least some fetching events
146+
if fetchingEvents == 0 {
147+
t.Error("Expected at least one 'fetching' event")
148+
}
149+
150+
// We should receive a final 'done' event
151+
if !finalDone {
152+
t.Error("Expected final 'done' event")
153+
}
154+
155+
// Processing events should roughly match fetching events (minus errors)
156+
// Allow some variance for errors
157+
if completeEvents == 0 && errorEvents == 0 {
158+
t.Error("Expected at least some 'complete' or 'error' events")
159+
}
160+
161+
// Check that the final event is 'done'
162+
if len(events) > 0 && events[len(events)-1].Type != "done" {
163+
t.Errorf("Expected final event to be 'done', got '%s'", events[len(events)-1].Type)
164+
}
165+
166+
// Verify response headers for SSE
167+
contentType := w.Header().Get("Content-Type")
168+
if contentType != "text/event-stream" {
169+
t.Errorf("Content-Type = %s, want text/event-stream", contentType)
170+
}
171+
172+
cacheControl := w.Header().Get("Cache-Control")
173+
if cacheControl != "no-cache" {
174+
t.Errorf("Cache-Control = %s, want no-cache", cacheControl)
175+
}
176+
177+
connection := w.Header().Get("Connection")
178+
if connection != "keep-alive" {
179+
t.Errorf("Connection = %s, want keep-alive", connection)
180+
}
181+
}
182+
183+
// TestOrgSampleStreamNoTimeout verifies the stream doesn't timeout during long operations.
184+
func TestOrgSampleStreamNoTimeout(t *testing.T) {
185+
// Skip if no GitHub token is available
186+
token := os.Getenv("GITHUB_TOKEN")
187+
if token == "" {
188+
t.Skip("GITHUB_TOKEN not set, skipping integration test")
189+
}
190+
191+
// Create server
192+
s := New()
193+
s.SetRateLimit(1000, 1000)
194+
195+
// Create request with larger sample size to ensure longer operation
196+
reqBody := OrgSampleRequest{
197+
Org: "codeGROOVE-dev",
198+
SampleSize: 25,
199+
Days: 90,
200+
}
201+
body, err := json.Marshal(reqBody)
202+
if err != nil {
203+
t.Fatalf("Failed to marshal request: %v", err)
204+
}
205+
206+
req := httptest.NewRequest(http.MethodPost, "/v1/calculate/org/stream", bytes.NewReader(body))
207+
req.Header.Set("Content-Type", "application/json")
208+
req.Header.Set("Authorization", "Bearer "+token)
209+
210+
w := httptest.NewRecorder()
211+
212+
// Track time
213+
start := time.Now()
214+
215+
// Run handler
216+
done := make(chan bool)
217+
var receivedEvents int
218+
219+
go func() {
220+
s.handleOrgSampleStream(w, req)
221+
222+
// Count events
223+
scanner := bufio.NewScanner(strings.NewReader(w.Body.String()))
224+
for scanner.Scan() {
225+
line := scanner.Text()
226+
if strings.HasPrefix(line, "data: ") {
227+
receivedEvents++
228+
}
229+
}
230+
close(done)
231+
}()
232+
233+
// Wait with generous timeout (5 minutes for 20 PRs)
234+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
235+
defer cancel()
236+
237+
select {
238+
case <-done:
239+
elapsed := time.Since(start)
240+
t.Logf("Stream completed in %v with %d events", elapsed, receivedEvents)
241+
242+
if receivedEvents == 0 {
243+
t.Error("No events received - stream may have timed out")
244+
}
245+
246+
case <-ctx.Done():
247+
t.Fatal("Stream timed out after 5 minutes - this indicates a problem with context cancellation")
248+
}
249+
}

internal/server/static/favicon.ico

159 KB
Binary file not shown.

internal/server/static/logo.png

253 KB
Loading

media/favicon.ico

159 KB
Binary file not shown.

media/logo-small.png

34.6 KB
Loading

media/logo.png

253 KB
Loading

0 commit comments

Comments
 (0)