|
9 | 9 | transport_type: :streamable, |
10 | 10 | request_timeout: 5000, |
11 | 11 | config: { |
12 | | - url: "http://localhost:3005/mcp" |
| 12 | + url: TestServerManager::HTTP_SERVER_URL |
13 | 13 | } |
14 | 14 | ) |
15 | 15 | end |
16 | 16 |
|
17 | 17 | let(:mock_coordinator) { instance_double(RubyLLM::MCP::Coordinator) } |
18 | 18 | let(:transport) do |
19 | 19 | RubyLLM::MCP::Transport::StreamableHTTP.new( |
20 | | - "http://localhost:3005/mcp", |
| 20 | + TestServerManager::HTTP_SERVER_URL, |
21 | 21 | request_timeout: 5000, |
22 | 22 | coordinator: mock_coordinator |
23 | 23 | ) |
|
166 | 166 |
|
167 | 167 | describe "connection errors" do |
168 | 168 | it "handles connection refused errors" do |
169 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 169 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
170 | 170 | .to_raise(Errno::ECONNREFUSED) |
171 | 171 |
|
172 | 172 | expect do |
|
175 | 175 | end |
176 | 176 |
|
177 | 177 | it "handles timeout errors" do |
178 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 178 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
179 | 179 | .to_timeout |
180 | 180 |
|
181 | 181 | expect do |
|
184 | 184 | end |
185 | 185 |
|
186 | 186 | it "handles network errors" do |
187 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 187 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
188 | 188 | .to_raise(SocketError.new("Failed to open TCP connection")) |
189 | 189 |
|
190 | 190 | expect do |
|
195 | 195 |
|
196 | 196 | describe "HTTP status errors" do |
197 | 197 | it "handles 400 Bad Request with JSON error" do |
198 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 198 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
199 | 199 | .to_return( |
200 | 200 | status: 400, |
201 | 201 | headers: { "Content-Type" => "application/json" }, |
|
208 | 208 | end |
209 | 209 |
|
210 | 210 | it "handles 400 Bad Request with malformed JSON error" do |
211 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 211 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
212 | 212 | .to_return( |
213 | 213 | status: 400, |
214 | 214 | headers: { "Content-Type" => "application/json" }, |
|
221 | 221 | end |
222 | 222 |
|
223 | 223 | it "handles 401 Unauthorized" do |
224 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 224 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
225 | 225 | .to_return(status: 401) |
226 | 226 |
|
227 | 227 | result = transport.request({ "method" => "initialize", "id" => 1 }, wait_for_response: false) |
228 | 228 | expect(result).to be_nil |
229 | 229 | end |
230 | 230 |
|
231 | 231 | it "handles 404 Not Found (session expired)" do |
232 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 232 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
233 | 233 | .to_return(status: 404) |
234 | 234 |
|
235 | 235 | expect do |
|
238 | 238 | end |
239 | 239 |
|
240 | 240 | it "handles 405 Method Not Allowed" do |
241 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 241 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
242 | 242 | .to_return(status: 405) |
243 | 243 |
|
244 | 244 | result = transport.request({ "method" => "unsupported", "id" => 1 }, wait_for_response: false) |
245 | 245 | expect(result).to be_nil |
246 | 246 | end |
247 | 247 |
|
248 | 248 | it "handles 500 Internal Server Error" do |
249 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 249 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
250 | 250 | .to_return( |
251 | 251 | status: 500, |
252 | 252 | body: "Internal Server Error" |
|
258 | 258 | end |
259 | 259 |
|
260 | 260 | it "handles session-related errors in error message" do |
261 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 261 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
262 | 262 | .to_return( |
263 | 263 | status: 400, |
264 | 264 | headers: { "Content-Type" => "application/json" }, |
|
275 | 275 |
|
276 | 276 | describe "response content errors" do |
277 | 277 | it "handles invalid JSON in successful response" do |
278 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 278 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
279 | 279 | .to_return( |
280 | 280 | status: 200, |
281 | 281 | headers: { "Content-Type" => "application/json" }, |
|
288 | 288 | end |
289 | 289 |
|
290 | 290 | it "handles unexpected content type" do |
291 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 291 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
292 | 292 | .to_return( |
293 | 293 | status: 200, |
294 | 294 | headers: { "Content-Type" => "text/plain" }, |
|
303 | 303 |
|
304 | 304 | describe "SSE (Server-Sent Events) errors" do |
305 | 305 | it "handles SSE 400 errors" do |
306 | | - stub_request(:get, "http://localhost:3005/mcp") |
| 306 | + stub_request(:get, TestServerManager::HTTP_SERVER_URL) |
307 | 307 | .with(headers: { "Accept" => "text/event-stream" }) |
308 | 308 | .to_return(status: 400) |
309 | 309 |
|
|
315 | 315 | end |
316 | 316 |
|
317 | 317 | it "handles SSE 405 Method Not Allowed gracefully" do |
318 | | - stub_request(:get, "http://localhost:3005/mcp") |
| 318 | + stub_request(:get, TestServerManager::HTTP_SERVER_URL) |
319 | 319 | .with(headers: { "Accept" => "text/event-stream" }) |
320 | 320 | .to_return(status: 405) |
321 | 321 |
|
|
405 | 405 | it "handles session termination failure" do |
406 | 406 | transport.instance_variable_set(:@session_id, "test-session") |
407 | 407 |
|
408 | | - stub_request(:delete, "http://localhost:3005/mcp") |
| 408 | + stub_request(:delete, TestServerManager::HTTP_SERVER_URL) |
409 | 409 | .to_return(status: 500, body: "Server Error") |
410 | 410 |
|
411 | 411 | expect do |
|
416 | 416 | it "handles session termination connection error" do |
417 | 417 | transport.instance_variable_set(:@session_id, "test-session") |
418 | 418 |
|
419 | | - stub_request(:delete, "http://localhost:3005/mcp") |
| 419 | + stub_request(:delete, TestServerManager::HTTP_SERVER_URL) |
420 | 420 | .to_raise(Errno::ECONNREFUSED) |
421 | 421 |
|
422 | 422 | expect do |
|
427 | 427 | it "accepts 405 status for session termination" do |
428 | 428 | transport.instance_variable_set(:@session_id, "test-session") |
429 | 429 |
|
430 | | - stub_request(:delete, "http://localhost:3005/mcp") |
| 430 | + stub_request(:delete, TestServerManager::HTTP_SERVER_URL) |
431 | 431 | .to_return(status: 405) |
432 | 432 |
|
433 | 433 | # Should not raise an error for 405 (acceptable per spec) |
|
442 | 442 | transport.instance_variable_set(:@session_id, nil) |
443 | 443 |
|
444 | 444 | # Should return early without making any requests |
445 | | - expect(WebMock).not_to have_requested(:delete, "http://localhost:3005/mcp") |
| 445 | + expect(WebMock).not_to have_requested(:delete, TestServerManager::HTTP_SERVER_URL) |
446 | 446 |
|
447 | 447 | transport.send(:terminate_session) |
448 | 448 | end |
|
451 | 451 | before do |
452 | 452 | transport.instance_variable_set(:@session_id, "test-session") |
453 | 453 |
|
454 | | - stub_request(:delete, "http://localhost:3005/mcp") |
| 454 | + stub_request(:delete, TestServerManager::HTTP_SERVER_URL) |
455 | 455 | .to_return(status: 400, body: "Bad Request") |
456 | 456 | end |
457 | 457 |
|
|
540 | 540 | describe "202 Accepted response handling" do |
541 | 541 | it "starts SSE stream on initialization with 202" do |
542 | 542 | allow(transport).to receive(:start_sse_stream) |
543 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 543 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
544 | 544 | .to_return(status: 202) |
545 | 545 |
|
546 | 546 | transport.request({ "method" => "initialize", "id" => 1 }, wait_for_response: false) |
|
550 | 550 |
|
551 | 551 | it "does not start SSE stream on non-initialization 202" do |
552 | 552 | allow(transport).to receive(:start_sse_stream) |
553 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 553 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
554 | 554 | .to_return(status: 202) |
555 | 555 |
|
556 | 556 | result = transport.request({ "method" => "other", "id" => 1 }, wait_for_response: false) |
|
573 | 573 |
|
574 | 574 | let(:transport_with_options) do |
575 | 575 | RubyLLM::MCP::Transport::StreamableHTTP.new( |
576 | | - "http://localhost:3005/mcp", |
| 576 | + TestServerManager::HTTP_SERVER_URL, |
577 | 577 | request_timeout: 5000, |
578 | 578 | coordinator: mock_coordinator, |
579 | 579 | reconnection_options: reconnection_options |
|
592 | 592 | let(:reconnection_options) { RubyLLM::MCP::Transport::ReconnectionOptions.new(max_retries: 1) } |
593 | 593 | let(:transport_with_options) do |
594 | 594 | RubyLLM::MCP::Transport::StreamableHTTP.new( |
595 | | - "http://localhost:3005/mcp", |
| 595 | + TestServerManager::HTTP_SERVER_URL, |
596 | 596 | request_timeout: 1000, |
597 | 597 | coordinator: mock_coordinator, |
598 | 598 | reconnection_options: reconnection_options |
599 | 599 | ) |
600 | 600 | end |
601 | 601 |
|
602 | 602 | before do |
603 | | - stub_request(:get, "http://localhost:3005/mcp") |
| 603 | + stub_request(:get, TestServerManager::HTTP_SERVER_URL) |
604 | 604 | .with(headers: { "Accept" => "text/event-stream" }) |
605 | 605 | .to_raise(Errno::ECONNREFUSED) |
606 | 606 | end |
|
617 | 617 | it "stops retrying when transport is closed" do |
618 | 618 | transport.instance_variable_set(:@running, false) |
619 | 619 |
|
620 | | - stub_request(:get, "http://localhost:3005/mcp") |
| 620 | + stub_request(:get, TestServerManager::HTTP_SERVER_URL) |
621 | 621 | .with(headers: { "Accept" => "text/event-stream" }) |
622 | 622 | .to_raise(Errno::ECONNREFUSED) |
623 | 623 |
|
|
628 | 628 | end.to raise_error(RubyLLM::MCP::Errors::TransportError, /Connection refused/) |
629 | 629 | end |
630 | 630 |
|
| 631 | + it "returns a 400 error if server is not running" do |
| 632 | + transport.instance_variable_set(:@running, false) |
| 633 | + |
| 634 | + stub_request(:get, "http://fakeurl:4000/mcp") |
| 635 | + .with(headers: { "Accept" => "text/event-stream" }) |
| 636 | + .to_raise(Errno::ECONNREFUSED) |
| 637 | + |
| 638 | + options = RubyLLM::MCP::Transport::StartSSEOptions.new |
| 639 | + |
| 640 | + expect do |
| 641 | + transport.send(:start_sse, options) |
| 642 | + end.to raise_error(RubyLLM::MCP::Errors::TransportError, /Failed to open SSE stream: 400/) |
| 643 | + end |
| 644 | + |
631 | 645 | it "stops retrying when abort controller is set" do |
632 | 646 | transport.instance_variable_set(:@abort_controller, true) |
633 | 647 |
|
634 | | - stub_request(:get, "http://localhost:3005/mcp") |
| 648 | + stub_request(:get, TestServerManager::HTTP_SERVER_URL) |
635 | 649 | .with(headers: { "Accept" => "text/event-stream" }) |
636 | 650 | .to_raise(Errno::ECONNREFUSED) |
637 | 651 |
|
|
645 | 659 |
|
646 | 660 | describe "edge cases and boundary conditions" do |
647 | 661 | it "handles bad JSON format request body gracefully" do |
648 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 662 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
649 | 663 | .to_return( |
650 | 664 | status: 200, |
651 | 665 | headers: { "Content-Type" => "application/json" }, |
|
661 | 675 | it "handles request without ID gracefully" do |
662 | 676 | session_id = SecureRandom.uuid |
663 | 677 |
|
664 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 678 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
665 | 679 | .to_return( |
666 | 680 | status: 200, |
667 | 681 | headers: { "Content-Type" => "application/json", "mcp-session-id" => session_id }, |
|
676 | 690 | it "handles very large response gracefully" do |
677 | 691 | large_response = { "result" => { "content" => [{ "type" => "text", "value" => "x" * 10_000 }] } } |
678 | 692 |
|
679 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 693 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
680 | 694 | .to_return( |
681 | 695 | status: 200, |
682 | 696 | headers: { "Content-Type" => "application/json" }, |
|
690 | 704 |
|
691 | 705 | it "handles response with event-stream content type" do |
692 | 706 | allow(transport).to receive(:start_sse_stream) |
693 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 707 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
694 | 708 | .to_return( |
695 | 709 | status: 200, |
696 | 710 | headers: { "Content-Type" => "text/event-stream" }, |
|
705 | 719 |
|
706 | 720 | context "when handling session ID extraction from response headers" do |
707 | 721 | before do |
708 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 722 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
709 | 723 | .to_return( |
710 | 724 | status: 200, |
711 | 725 | headers: { |
|
724 | 738 |
|
725 | 739 | context "when response has malformed JSON" do |
726 | 740 | before do |
727 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 741 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
728 | 742 | .to_return( |
729 | 743 | status: 200, |
730 | 744 | headers: { "Content-Type" => "application/json" }, |
|
741 | 755 |
|
742 | 756 | context "when handling HTTPX error response in main request" do |
743 | 757 | before do |
744 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 758 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
745 | 759 | .to_raise(Net::ReadTimeout.new("Connection timeout")) |
746 | 760 | end |
747 | 761 |
|
|
754 | 768 |
|
755 | 769 | context "when HTTPX error response has no error message" do |
756 | 770 | before do |
757 | | - stub_request(:post, "http://localhost:3005/mcp") |
| 771 | + stub_request(:post, TestServerManager::HTTP_SERVER_URL) |
758 | 772 | .to_return(status: 500, body: "Internal Server Error") |
759 | 773 | end |
760 | 774 |
|
|
0 commit comments