Skip to content

Commit 89ddeb0

Browse files
shivasuryaclaude
andcommitted
test(mcp): add comprehensive tests for analytics, server, and http
Improve test coverage from 88.8% to 95.6%: - Add analytics_test.go with tests for all Analytics methods - Add server tests for SetTransport, handleStatus, GetStatusTracker, IsReady - Add HTTP tests for Start blocking, SSE endpoint, invalid addresses - Add streaming handler tests for write error paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent df7c5f3 commit 89ddeb0

File tree

3 files changed

+397
-0
lines changed

3 files changed

+397
-0
lines changed

sast-engine/mcp/analytics_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package mcp
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestNewAnalytics(t *testing.T) {
11+
a := NewAnalytics("stdio")
12+
13+
assert.NotNil(t, a)
14+
assert.Equal(t, "stdio", a.transport)
15+
assert.False(t, a.startTime.IsZero())
16+
}
17+
18+
func TestNewAnalytics_HTTP(t *testing.T) {
19+
a := NewAnalytics("http")
20+
21+
assert.Equal(t, "http", a.transport)
22+
}
23+
24+
func TestAnalytics_ReportServerStarted(t *testing.T) {
25+
a := NewAnalytics("stdio")
26+
27+
// Should not panic even with analytics disabled.
28+
a.ReportServerStarted()
29+
}
30+
31+
func TestAnalytics_ReportServerStopped(t *testing.T) {
32+
a := NewAnalytics("http")
33+
34+
// Should not panic.
35+
a.ReportServerStopped()
36+
}
37+
38+
func TestAnalytics_ReportToolCall(t *testing.T) {
39+
a := NewAnalytics("stdio")
40+
41+
// Should not panic.
42+
a.ReportToolCall("find_symbol", 150, true)
43+
a.ReportToolCall("get_callers", 50, false)
44+
}
45+
46+
func TestAnalytics_ReportIndexingStarted(t *testing.T) {
47+
a := NewAnalytics("stdio")
48+
49+
// Should not panic.
50+
a.ReportIndexingStarted()
51+
}
52+
53+
func TestAnalytics_ReportIndexingComplete(t *testing.T) {
54+
a := NewAnalytics("http")
55+
56+
stats := &IndexingStats{
57+
Functions: 100,
58+
CallEdges: 500,
59+
Modules: 20,
60+
Files: 50,
61+
BuildDuration: 5 * time.Second,
62+
}
63+
64+
// Should not panic.
65+
a.ReportIndexingComplete(stats)
66+
}
67+
68+
func TestAnalytics_ReportIndexingFailed(t *testing.T) {
69+
a := NewAnalytics("stdio")
70+
71+
// Should not panic.
72+
a.ReportIndexingFailed("parsing")
73+
a.ReportIndexingFailed("call_graph")
74+
}
75+
76+
func TestAnalytics_ReportClientConnected(t *testing.T) {
77+
a := NewAnalytics("http")
78+
79+
// Should not panic.
80+
a.ReportClientConnected("claude-code", "1.0.0")
81+
a.ReportClientConnected("", "")
82+
}
83+
84+
func TestAnalytics_StartToolCall(t *testing.T) {
85+
a := NewAnalytics("stdio")
86+
87+
metrics := a.StartToolCall("find_symbol")
88+
89+
assert.NotNil(t, metrics)
90+
assert.Equal(t, "find_symbol", metrics.ToolName)
91+
assert.False(t, metrics.StartTime.IsZero())
92+
}
93+
94+
func TestAnalytics_EndToolCall(t *testing.T) {
95+
a := NewAnalytics("stdio")
96+
97+
metrics := a.StartToolCall("get_callers")
98+
time.Sleep(1 * time.Millisecond) // Small delay to ensure duration > 0
99+
100+
// Should not panic.
101+
a.EndToolCall(metrics, true)
102+
}
103+
104+
func TestAnalytics_EndToolCall_Nil(t *testing.T) {
105+
a := NewAnalytics("stdio")
106+
107+
// Should not panic with nil metrics.
108+
a.EndToolCall(nil, true)
109+
}
110+
111+
func TestAnalytics_EndToolCall_Failed(t *testing.T) {
112+
a := NewAnalytics("http")
113+
114+
metrics := a.StartToolCall("resolve_import")
115+
116+
// Should not panic.
117+
a.EndToolCall(metrics, false)
118+
}
119+
120+
func TestToolCallMetrics(t *testing.T) {
121+
metrics := &ToolCallMetrics{
122+
StartTime: time.Now(),
123+
ToolName: "test_tool",
124+
}
125+
126+
assert.Equal(t, "test_tool", metrics.ToolName)
127+
assert.False(t, metrics.StartTime.IsZero())
128+
}

sast-engine/mcp/http_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,192 @@ func TestHTTPServer_NoOriginHeader(t *testing.T) {
465465
// Should use "*" as fallback.
466466
assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin"))
467467
}
468+
469+
func TestHTTPServer_Start_AlreadyRunning(t *testing.T) {
470+
mcpServer := createTestServer()
471+
config := &HTTPConfig{Address: "127.0.0.1:0"}
472+
httpServer := NewHTTPServer(mcpServer, config)
473+
474+
// Start async first.
475+
err := httpServer.StartAsync()
476+
require.NoError(t, err)
477+
defer func() {
478+
ctx := context.Background()
479+
_ = httpServer.Shutdown(ctx)
480+
}()
481+
482+
// Try to start synchronously - should fail.
483+
err = httpServer.Start()
484+
assert.Error(t, err)
485+
assert.Contains(t, err.Error(), "already running")
486+
}
487+
488+
func TestHTTPServer_Start_Blocking(t *testing.T) {
489+
mcpServer := createTestServer()
490+
config := &HTTPConfig{Address: "127.0.0.1:0"}
491+
httpServer := NewHTTPServer(mcpServer, config)
492+
493+
// Start in goroutine since it blocks.
494+
errCh := make(chan error, 1)
495+
go func() {
496+
errCh <- httpServer.Start()
497+
}()
498+
499+
// Give server time to start.
500+
time.Sleep(50 * time.Millisecond)
501+
502+
// Verify it's running.
503+
assert.True(t, httpServer.IsRunning())
504+
505+
// Shutdown.
506+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
507+
defer cancel()
508+
err := httpServer.Shutdown(ctx)
509+
require.NoError(t, err)
510+
511+
// Wait for Start() to return.
512+
select {
513+
case err := <-errCh:
514+
// Server closed error is expected.
515+
assert.Contains(t, err.Error(), "Server closed")
516+
case <-time.After(5 * time.Second):
517+
t.Fatal("timeout waiting for Start to return")
518+
}
519+
}
520+
521+
func TestSSEServer_ServeSSE(t *testing.T) {
522+
mcpServer := createTestServer()
523+
httpServer := NewHTTPServer(mcpServer, nil)
524+
sseServer := NewSSEServer(httpServer)
525+
526+
// Create a request with a cancellable context.
527+
ctx, cancel := context.WithCancel(context.Background())
528+
req := httptest.NewRequest(http.MethodGet, "/sse", nil).WithContext(ctx)
529+
req.Header.Set("Origin", "http://example.com")
530+
531+
rec := httptest.NewRecorder()
532+
533+
// Run ServeSSE in a goroutine since it blocks.
534+
done := make(chan struct{})
535+
go func() {
536+
sseServer.ServeSSE(rec, req)
537+
close(done)
538+
}()
539+
540+
// Give it time to set headers and send connected event.
541+
time.Sleep(50 * time.Millisecond)
542+
543+
// Cancel context to close connection.
544+
cancel()
545+
546+
// Wait for ServeSSE to return.
547+
select {
548+
case <-done:
549+
// Good, it returned.
550+
case <-time.After(5 * time.Second):
551+
t.Fatal("timeout waiting for ServeSSE to return")
552+
}
553+
554+
// Verify SSE headers.
555+
assert.Equal(t, "text/event-stream", rec.Header().Get("Content-Type"))
556+
assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
557+
assert.Equal(t, "keep-alive", rec.Header().Get("Connection"))
558+
559+
// Verify connected event was sent.
560+
assert.Contains(t, rec.Body.String(), "event: connected")
561+
assert.Contains(t, rec.Body.String(), "status")
562+
}
563+
564+
// mockResponseWriter doesn't implement http.Flusher.
565+
type noFlushResponseWriter struct {
566+
http.ResponseWriter
567+
}
568+
569+
func TestSSEServer_ServeSSE_NoFlusher(t *testing.T) {
570+
mcpServer := createTestServer()
571+
httpServer := NewHTTPServer(mcpServer, nil)
572+
sseServer := NewSSEServer(httpServer)
573+
574+
req := httptest.NewRequest(http.MethodGet, "/sse", nil)
575+
576+
// Use a writer that doesn't implement Flusher.
577+
rec := httptest.NewRecorder()
578+
noFlush := &noFlushResponseWriter{rec}
579+
580+
sseServer.ServeSSE(noFlush, req)
581+
582+
// Should return error response.
583+
assert.Equal(t, http.StatusInternalServerError, rec.Code)
584+
assert.Contains(t, rec.Body.String(), "SSE not supported")
585+
}
586+
587+
func TestHTTPServer_Shutdown_WithContext(t *testing.T) {
588+
mcpServer := createTestServer()
589+
config := &HTTPConfig{Address: "127.0.0.1:0"}
590+
httpServer := NewHTTPServer(mcpServer, config)
591+
592+
err := httpServer.StartAsync()
593+
require.NoError(t, err)
594+
595+
// Shutdown with cancelled context should still work.
596+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
597+
defer cancel()
598+
599+
err = httpServer.Shutdown(ctx)
600+
assert.NoError(t, err)
601+
assert.False(t, httpServer.IsRunning())
602+
}
603+
604+
func TestHTTPServer_Start_InvalidAddress(t *testing.T) {
605+
mcpServer := createTestServer()
606+
// Use an invalid address that should fail to listen.
607+
config := &HTTPConfig{Address: "invalid:address:format:99999"}
608+
httpServer := NewHTTPServer(mcpServer, config)
609+
610+
err := httpServer.Start()
611+
assert.Error(t, err)
612+
assert.Contains(t, err.Error(), "failed to listen")
613+
assert.False(t, httpServer.IsRunning())
614+
}
615+
616+
func TestHTTPServer_StartAsync_InvalidAddress(t *testing.T) {
617+
mcpServer := createTestServer()
618+
// Use an invalid address that should fail to listen.
619+
config := &HTTPConfig{Address: "invalid:address:format:99999"}
620+
httpServer := NewHTTPServer(mcpServer, config)
621+
622+
err := httpServer.StartAsync()
623+
assert.Error(t, err)
624+
assert.Contains(t, err.Error(), "failed to listen")
625+
assert.False(t, httpServer.IsRunning())
626+
}
627+
628+
// errorWriter always returns an error.
629+
type errorWriter struct{}
630+
631+
func (e *errorWriter) Write(p []byte) (n int, err error) {
632+
return 0, io.ErrClosedPipe
633+
}
634+
635+
func TestStreamingHTTPHandler_HandleStream_WriteError(t *testing.T) {
636+
mcpServer := createTestServer()
637+
handler := NewStreamingHTTPHandler(mcpServer)
638+
639+
input := `{"jsonrpc":"2.0","id":1,"method":"ping"}` + "\n"
640+
reader := strings.NewReader(input)
641+
642+
err := handler.HandleStream(reader, &errorWriter{})
643+
assert.Error(t, err)
644+
}
645+
646+
func TestStreamingHTTPHandler_HandleStream_WriteErrorOnParseError(t *testing.T) {
647+
mcpServer := createTestServer()
648+
handler := NewStreamingHTTPHandler(mcpServer)
649+
650+
// Invalid JSON to trigger parse error, which then tries to write.
651+
input := "not valid json\n"
652+
reader := strings.NewReader(input)
653+
654+
err := handler.HandleStream(reader, &errorWriter{})
655+
assert.Error(t, err)
656+
}

0 commit comments

Comments
 (0)