Skip to content

Commit e61344a

Browse files
committed
test: Full test coverage for run.oy
#15 Branch: Run-15 Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>
1 parent e1cdba9 commit e61344a

1 file changed

Lines changed: 259 additions & 0 deletions

File tree

tests/commands/server/test_run.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,262 @@ def test_run_registration_with_grpc_source(self) -> None:
476476
mock_process.assert_called_once()
477477
call_args = mock_process.call_args[1]
478478
assert call_args.get("target") is mock_translate
479+
480+
def test_run_register_without_source_warns(self) -> None:
481+
"""Test that register=True without stdio or grpc prints a warning."""
482+
with (
483+
patch("mcpgateway.translate.main"),
484+
patch("multiprocessing.Process") as mock_process,
485+
patch("cforge.commands.server.run.get_console") as mock_console,
486+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
487+
):
488+
mock_console_instance = MagicMock()
489+
mock_console.return_value = mock_console_instance
490+
491+
# No stdio or grpc, but register=True
492+
invoke_typer_command(run, register=True)
493+
494+
# Verify warning was printed about needing stdio or grpc
495+
assert any("Warning" in str(call) and "register" in str(call).lower() for call in mock_console_instance.print.call_args_list)
496+
497+
# Verify registration was NOT attempted (since it was disabled)
498+
mock_request.assert_not_called()
499+
500+
# Verify translate_main was still called via Process
501+
mock_process.assert_called_once()
502+
503+
def test_run_health_check_connection_error_retry(self) -> None:
504+
"""Test that health check retries on connection errors."""
505+
import requests as real_requests
506+
507+
with (
508+
patch("mcpgateway.translate.main"),
509+
patch("multiprocessing.Process") as mock_process,
510+
patch("cforge.commands.server.run.requests") as mock_requests,
511+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
512+
patch("cforge.commands.server.run.time") as mock_time,
513+
):
514+
# First call raises ConnectionError, second succeeds
515+
mock_get_res = MagicMock()
516+
mock_get_res.status_code = 200
517+
mock_requests.get = MagicMock(
518+
side_effect=[
519+
real_requests.exceptions.ConnectionError("Connection refused"),
520+
mock_get_res,
521+
]
522+
)
523+
mock_requests.exceptions = real_requests.exceptions
524+
525+
# Mock time to control the loop
526+
mock_time.time = MagicMock(side_effect=[0, 0.5, 1])
527+
mock_time.sleep = MagicMock()
528+
529+
mock_request.return_value = {"id": "test-server-id"}
530+
531+
invoke_typer_command(run, stdio="uvx mcp-server-git", port=9000, register=True)
532+
533+
# Verify health check was retried
534+
assert mock_requests.get.call_count == 2
535+
536+
# Verify sleep was called after connection error
537+
mock_time.sleep.assert_called_once_with(0.5)
538+
539+
# Verify registration succeeded after retry
540+
mock_request.assert_called_once()
541+
mock_process.assert_called_once()
542+
543+
def test_run_health_check_timeout(self) -> None:
544+
"""Test that health check timeout exits with error."""
545+
import requests as real_requests
546+
547+
import typer
548+
549+
with (
550+
patch("mcpgateway.translate.main"),
551+
patch("multiprocessing.Process"),
552+
patch("cforge.commands.server.run.requests") as mock_requests,
553+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
554+
patch("cforge.commands.server.run.time") as mock_time,
555+
patch("cforge.commands.server.run.get_console") as mock_console,
556+
patch("cforge.commands.server.run.typer.exit", side_effect=typer.Exit(1)) as mock_exit,
557+
):
558+
# Always raise ConnectionError
559+
mock_requests.get = MagicMock(side_effect=real_requests.exceptions.ConnectionError("Connection refused"))
560+
mock_requests.exceptions = real_requests.exceptions
561+
562+
# Mock time to simulate timeout
563+
mock_time.time = MagicMock(side_effect=[0, 5, 11]) # Start, after first try, after timeout
564+
mock_time.sleep = MagicMock()
565+
566+
mock_console_instance = MagicMock()
567+
mock_console.return_value = mock_console_instance
568+
569+
invoke_typer_command(run, stdio="uvx mcp-server-git", port=9000, register=True, register_timeout=10.0)
570+
571+
# Verify timeout error message was printed
572+
assert any("Failed to connect" in str(call) for call in mock_console_instance.print.call_args_list)
573+
574+
# Verify typer.exit was called with error code
575+
mock_exit.assert_called_once_with(1)
576+
577+
# Registration should not have been attempted
578+
mock_request.assert_not_called()
579+
580+
def test_run_temporary_cleanup_success(self) -> None:
581+
"""Test that temporary server cleanup function works correctly."""
582+
with (
583+
patch("mcpgateway.translate.main"),
584+
patch("multiprocessing.Process"),
585+
patch("cforge.commands.server.run.requests") as mock_requests,
586+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
587+
patch("cforge.commands.server.run.atexit") as mock_atexit,
588+
patch("cforge.commands.server.run.get_console") as mock_console,
589+
):
590+
# Mock returning a 200 on health
591+
mock_get_res = MagicMock()
592+
mock_get_res.status_code = 200
593+
mock_requests.get = MagicMock(return_value=mock_get_res)
594+
595+
mock_request.return_value = {"id": "temp-server-id", "name": "temp-server"}
596+
597+
mock_console_instance = MagicMock()
598+
mock_console.return_value = mock_console_instance
599+
600+
invoke_typer_command(run, stdio="uvx mcp-server-git", port=9000, temporary=True)
601+
602+
# Verify cleanup handler was registered
603+
mock_atexit.register.assert_called_once()
604+
605+
# Get the cleanup function and call it
606+
cleanup_fn = mock_atexit.register.call_args[0][0]
607+
608+
# Reset mock_request to track cleanup call
609+
mock_request.reset_mock()
610+
611+
# Call cleanup function
612+
cleanup_fn()
613+
614+
# Verify unregistration was attempted
615+
mock_request.assert_called_once()
616+
call_args = mock_request.call_args
617+
assert call_args[0][0] == "DELETE"
618+
assert "/gateways/temp-server-id" in call_args[0][1]
619+
620+
def test_run_temporary_cleanup_failure(self) -> None:
621+
"""Test that temporary server cleanup handles errors gracefully."""
622+
with (
623+
patch("mcpgateway.translate.main"),
624+
patch("multiprocessing.Process"),
625+
patch("cforge.commands.server.run.requests") as mock_requests,
626+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
627+
patch("cforge.commands.server.run.atexit") as mock_atexit,
628+
patch("cforge.commands.server.run.get_console") as mock_console,
629+
):
630+
# Mock returning a 200 on health
631+
mock_get_res = MagicMock()
632+
mock_get_res.status_code = 200
633+
mock_requests.get = MagicMock(return_value=mock_get_res)
634+
635+
mock_request.return_value = {"id": "temp-server-id", "name": "temp-server"}
636+
637+
mock_console_instance = MagicMock()
638+
mock_console.return_value = mock_console_instance
639+
640+
invoke_typer_command(run, stdio="uvx mcp-server-git", port=9000, temporary=True)
641+
642+
# Get the cleanup function
643+
cleanup_fn = mock_atexit.register.call_args[0][0]
644+
645+
# Make the DELETE request fail
646+
mock_request.reset_mock()
647+
mock_request.side_effect = Exception("Network error")
648+
649+
# Call cleanup function - should not raise
650+
cleanup_fn()
651+
652+
# Verify warning was printed
653+
assert any("Warning" in str(call) and "unregister" in str(call).lower() for call in mock_console_instance.print.call_args_list)
654+
655+
def test_run_health_check_retries_on_non_200(self) -> None:
656+
"""Test that health check retries when server returns non-200 status."""
657+
with (
658+
patch("mcpgateway.translate.main"),
659+
patch("multiprocessing.Process") as mock_process,
660+
patch("cforge.commands.server.run.requests") as mock_requests,
661+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
662+
patch("cforge.commands.server.run.time") as mock_time,
663+
):
664+
# First call returns 503, second returns 200
665+
mock_get_res_503 = MagicMock()
666+
mock_get_res_503.status_code = 503
667+
mock_get_res_200 = MagicMock()
668+
mock_get_res_200.status_code = 200
669+
mock_requests.get = MagicMock(side_effect=[mock_get_res_503, mock_get_res_200])
670+
671+
# Mock time to control the loop
672+
mock_time.time = MagicMock(side_effect=[0, 0.5, 1])
673+
mock_time.sleep = MagicMock()
674+
675+
mock_request.return_value = {"id": "test-server-id"}
676+
677+
invoke_typer_command(run, stdio="uvx mcp-server-git", port=9000, register=True)
678+
679+
# Verify health check was retried (called twice)
680+
assert mock_requests.get.call_count == 2
681+
682+
# Verify registration succeeded
683+
mock_request.assert_called_once()
684+
mock_process.assert_called_once()
685+
686+
def test_run_registration_name_fallback_for_filtered_command(self) -> None:
687+
"""Test that server name falls back to stdio-server when all parts are filtered."""
688+
with (
689+
patch("mcpgateway.translate.main"),
690+
patch("multiprocessing.Process") as mock_process,
691+
patch("cforge.commands.server.run.requests") as mock_requests,
692+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
693+
):
694+
# Mock returning a 200 on health
695+
mock_get_res = MagicMock()
696+
mock_get_res.status_code = 200
697+
mock_requests.get = MagicMock(return_value=mock_get_res)
698+
699+
mock_request.return_value = {"id": "test-server-id"}
700+
701+
# Use a command where all parts get filtered (uvx, python, node, etc.)
702+
invoke_typer_command(run, stdio="uvx python node", port=9000, register=True)
703+
704+
# Verify name falls back to stdio-server-{port}
705+
call_args = mock_request.call_args
706+
json_data = call_args[1]["json_data"]
707+
assert json_data["name"] == "stdio-server-9000"
708+
709+
mock_process.assert_called_once()
710+
711+
def test_run_temporary_without_source_uses_fallback_name(self) -> None:
712+
"""Test that temporary registration without stdio/grpc uses fallback server name."""
713+
with (
714+
patch("mcpgateway.translate.main"),
715+
patch("multiprocessing.Process") as mock_process,
716+
patch("cforge.commands.server.run.requests") as mock_requests,
717+
patch("cforge.commands.server.run.make_authenticated_request") as mock_request,
718+
patch("cforge.commands.server.run.atexit") as mock_atexit,
719+
):
720+
# Mock returning a 200 on health
721+
mock_get_res = MagicMock()
722+
mock_get_res.status_code = 200
723+
mock_requests.get = MagicMock(return_value=mock_get_res)
724+
725+
mock_request.return_value = {"id": "temp-server-id"}
726+
727+
# temporary=True bypasses the stdio/grpc check, allowing registration without source
728+
invoke_typer_command(run, port=9000, temporary=True)
729+
730+
# Verify fallback name server-{port} was used
731+
call_args = mock_request.call_args
732+
json_data = call_args[1]["json_data"]
733+
assert json_data["name"] == "server-9000"
734+
735+
# Verify cleanup was registered
736+
mock_atexit.register.assert_called_once()
737+
mock_process.assert_called_once()

0 commit comments

Comments
 (0)