@@ -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