Files
connpy/connpy/tests/test_grpc_layer.py
T
fluzzi32 37db74f47d refactor(core): stabilize gRPC streaming, plugin invocation, and CLI UX
- Implement threaded plugin execution with Queue-based streaming in PluginService
- Refactor remote logger to preserve ANSI colors and fix TTY line endings (\r\n)
- Intelligent terminal filtering: disable SSM screen-clearing filter after success
- Sanitize SSH-only flags in core.py when using SFTP protocol
- Rewrite completion tree with pre/post-node states and flag deduplication
- Update gRPC unit tests to match new streaming response structure
2026-05-05 18:24:31 -03:00

203 lines
7.6 KiB
Python

import pytest
import grpc
import json
import os
import threading
from unittest.mock import MagicMock, patch
from concurrent import futures
from connpy.grpc_layer import server, connpy_pb2, connpy_pb2_grpc, stubs
from connpy.services.exceptions import ConnpyError
class MockContext:
def abort(self, code, details):
raise Exception(f"gRPC Abort: {code} - {details}")
# --- UNIT TESTS (with mocks) ---
class TestNodeServicerNaming:
@pytest.fixture
def servicer(self, populated_config):
return server.NodeServicer(populated_config)
@patch("connpy.core.node")
def test_interact_node_uses_passed_name(self, mock_node, servicer):
# Setup request with custom name
params = {"name": "custom-node-name@test", "host": "1.2.3.4", "protocol": "ssh"}
request = connpy_pb2.InteractRequest(
id="dynamic",
connection_params_json=json.dumps(params)
)
# Mock node to allow _connect
mock_node_instance = MagicMock()
mock_node_instance._connect.return_value = True
mock_node.return_value = mock_node_instance
# We only need the first iteration of the generator to check naming
gen = servicer.interact_node(iter([request]), MockContext())
next(gen) # Skip the success response
# Verify that node() was called with the custom name
mock_node.assert_called()
found = False
for call in mock_node.call_args_list:
if call.args[0] == "custom-node-name@test":
found = True
break
assert found
@patch("connpy.core.node")
def test_interact_node_fallback_naming(self, mock_node, servicer):
# Setup request without custom name but with host
params = {"host": "my-instance", "protocol": "ssm"}
request = connpy_pb2.InteractRequest(
id="dynamic",
connection_params_json=json.dumps(params)
)
mock_node_instance = MagicMock()
mock_node_instance._connect.return_value = True
mock_node.return_value = mock_node_instance
gen = servicer.interact_node(iter([request]), MockContext())
next(gen)
# Verify fallback name: dynamic-{host}@remote
found = False
for call in mock_node.call_args_list:
if call.args[0] == "dynamic-my-instance@remote":
found = True
break
assert found
class TestStubsMessageFormatting:
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("tty.setraw")
@patch("os.read")
@patch("select.select")
def test_connect_dynamic_msg_formatting_ssm(self, mock_select, mock_read, mock_setraw, mock_getattr, mock_setattr):
from connpy.grpc_layer.stubs import NodeStub
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
mock_channel = MagicMock()
stub = NodeStub(mock_channel, "localhost:8048")
mock_resp = MagicMock()
mock_resp.success = True
mock_resp.stdout_data = b''
stub.stub.interact_node.return_value = iter([mock_resp])
with patch("connpy.printer.success") as mock_success:
with patch("sys.stdin.fileno", return_value=0):
mock_select.return_value = ([], [], [])
params = {"protocol": "ssm", "host": "i-12345", "name": "my-ssm-node@aws"}
with patch("select.select", side_effect=KeyboardInterrupt):
try:
stub.connect_dynamic(params)
except KeyboardInterrupt:
pass
mock_success.assert_called()
msg = mock_success.call_args[0][0]
assert "Connected to my-ssm-node@aws" in msg
assert "at i-12345" in msg
assert ":22" not in msg
assert "via: ssm" in msg
# --- INTEGRATION TESTS (Real Server/Stub Communication) ---
class TestGRPCIntegration:
@pytest.fixture
def grpc_server(self, populated_config):
"""Starts a local gRPC server for integration testing."""
srv = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
# Register services
connpy_pb2_grpc.add_NodeServiceServicer_to_server(server.NodeServicer(populated_config), srv)
connpy_pb2_grpc.add_ProfileServiceServicer_to_server(server.ProfileServicer(populated_config), srv)
connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv)
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv)
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv)
port = srv.add_insecure_port('127.0.0.1:0')
srv.start()
yield f"127.0.0.1:{port}"
srv.stop(0)
@pytest.fixture
def channel(self, grpc_server):
with grpc.insecure_channel(grpc_server) as channel:
yield channel
@pytest.fixture
def node_stub(self, channel):
return stubs.NodeStub(channel, "localhost")
@pytest.fixture
def profile_stub(self, channel):
return stubs.ProfileStub(channel, "localhost")
@pytest.fixture
def config_stub(self, channel):
return stubs.ConfigStub(channel, "localhost")
def test_list_nodes_integration(self, node_stub):
nodes = node_stub.list_nodes()
assert "router1" in nodes
assert "server1@office" in nodes
def test_get_node_details_integration(self, node_stub):
details = node_stub.get_node_details("router1")
assert details["host"] == "10.0.0.1"
def test_node_not_found_integration(self, node_stub):
with pytest.raises(ConnpyError) as exc:
node_stub.get_node_details("non-existent")
assert "Node 'non-existent' not found." in str(exc.value)
def test_list_profiles_integration(self, profile_stub):
profiles = profile_stub.list_profiles()
assert "office-user" in profiles
def test_get_settings_integration(self, config_stub):
settings = config_stub.get_settings()
assert "idletime" in settings
def test_update_setting_integration(self, config_stub):
config_stub.update_setting("idletime", 99)
settings = config_stub.get_settings()
assert settings["idletime"] == 99
def test_add_delete_node_integration(self, node_stub):
node_stub.add_node("integration-test-node", {"host": "9.9.9.9"})
assert "integration-test-node" in node_stub.list_nodes()
node_stub.delete_node("integration-test-node")
assert "integration-test-node" not in node_stub.list_nodes()
def test_import_yaml_integration(self, channel, node_stub):
import yaml
from connpy.grpc_layer import stubs
stub = stubs.ImportExportStub(channel, "localhost")
# ImportExportService expects a flat dict of nodes, not a full config structure
inventory = {
"imported-node": {"host": "8.8.8.8", "protocol": "ssh", "type": "connection"}
}
yaml_content = yaml.dump(inventory)
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
stub.import_from_file(temp_path)
# Verify the node was imported and is visible via NodeStub
nodes = node_stub.list_nodes()
assert "imported-node" in nodes
finally:
if os.path.exists(temp_path):
os.remove(temp_path)