package server import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zzet/gortex/internal/mcp/streamable" ) // TestHandler_StreamableMCPRouteReturns404UntilWired — until // SetStreamableTransport runs, /mcp returns 404 (no route // registered). This is the load-bearing contract for "default off, // opt-in transport". func TestHandler_StreamableMCPRouteReturns404UntilWired(t *testing.T) { h := newTestHandler(t) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader([]byte(`{}`))) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) } // TestHandler_StreamableTransportMountedOnMCP exercises the full // server.Handler → streamable.Transport wiring path: SetStreamableTransport // installs the route, an inbound initialize POST mints a session and // the response header carries Mcp-Session-Id. func TestHandler_StreamableTransportMountedOnMCP(t *testing.T) { h := newTestHandler(t) store := streamable.NewMemoryStore(time.Minute) defer store.Close() transport := streamable.New(streamable.Config{ Dispatcher: streamable.MCPServerDispatcher{Server: h.mcpServer}, Store: store, }) h.SetStreamableTransport(transport) require.NotNil(t, h.StreamableTransport(), "transport getter must round-trip") body, _ := json.Marshal(map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{ "protocolVersion": "2026-03-26", "capabilities": map[string]any{}, "clientInfo": map[string]any{"name": "test", "version": "0.0.0"}, }, }) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code, "body=%s", rec.Body.String()) sid := rec.Header().Get(streamable.HeaderSessionID) require.NotEmpty(t, sid, "/mcp must set Mcp-Session-Id on initialize") require.True(t, strings.Contains(rec.Body.String(), `"jsonrpc":"2.0"`), "response missing JSON-RPC envelope: %s", rec.Body.String()) } // TestHandler_StreamableTransportDeleteSession proves the DELETE // branch reaches the transport (returns 204 even for unknown ids). func TestHandler_StreamableTransportDeleteSession(t *testing.T) { h := newTestHandler(t) store := streamable.NewMemoryStore(time.Minute) defer store.Close() transport := streamable.New(streamable.Config{ Dispatcher: streamable.MCPServerDispatcher{Server: h.mcpServer}, Store: store, }) h.SetStreamableTransport(transport) req := httptest.NewRequest(http.MethodDelete, "/mcp", nil) req.Header.Set(streamable.HeaderSessionID, "unknown") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code) } // TestHandler_SetRouterRouterAccessor — the router getter we added // for the streamable wireup has to round-trip the value too, // otherwise the daemon-side wireup silently loses routing. func TestHandler_SetRouterRouterAccessor(t *testing.T) { h := newTestHandler(t) assert.Nil(t, h.Router()) // We don't have a real router to wire here (Router has heavy // deps), but the nil round-trip is enough to prove the // accessor is wired to the right field. }