/ tests / unit / cli / commands / test_docs_cmd.py
test_docs_cmd.py
  1  """
  2  Unit tests for cli/commands/docs_cmd.py
  3  
  4  Tests the docs command including:
  5  - Starting documentation server with default port
  6  - Custom port configuration
  7  - Browser opening behavior
  8  - Error handling (missing docs directory)
  9  - HTTP request handler path rewriting
 10  - 404 redirect behavior
 11  - Keyboard interrupt handling
 12  """
 13  
 14  from unittest.mock import MagicMock, Mock, patch
 15  import pytest
 16  from click.testing import CliRunner
 17  
 18  from cli.commands.docs_cmd import docs, DocsHttpRequestHandler
 19  
 20  
 21  @pytest.fixture
 22  def runner():
 23      """Create a Click CLI runner for testing"""
 24      return CliRunner()
 25  
 26  
 27  @pytest.fixture
 28  def mock_docs_dir(tmp_path):
 29      """Create a temporary docs directory"""
 30      docs_dir = tmp_path / "docs"
 31      docs_dir.mkdir()
 32      (docs_dir / "index.html").write_text("<html>Test</html>")
 33      return docs_dir
 34  
 35  
 36  class TestDocsHttpRequestHandler:
 37      """Tests for the DocsHttpRequestHandler class"""
 38      
 39      def test_path_rewriting_with_solace_prefix(self):
 40          """Test that paths starting with /solace-agent-mesh are rewritten"""
 41          # Create a mock request with proper bytes
 42          mock_request = MagicMock()
 43          mock_request.makefile.return_value = MagicMock()
 44          
 45          # We need to test the path rewriting logic directly
 46          # Create handler without triggering __init__ fully
 47          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
 48          handler.path = "/solace-agent-mesh/docs/getting-started/"
 49          
 50          # Mock the parent do_GET to avoid actual HTTP handling
 51          with patch.object(DocsHttpRequestHandler.__bases__[0], 'do_GET'):
 52              handler.do_GET()
 53          
 54          assert handler.path == "/docs/getting-started/"
 55      
 56      def test_path_rewriting_root_path(self):
 57          """Test that /solace-agent-mesh alone becomes /"""
 58          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
 59          handler.path = "/solace-agent-mesh"
 60          
 61          with patch.object(DocsHttpRequestHandler.__bases__[0], 'do_GET'):
 62              handler.do_GET()
 63          
 64          assert handler.path == "/"
 65      
 66      def test_path_without_prefix_unchanged(self):
 67          """Test that paths without /solace-agent-mesh prefix are unchanged"""
 68          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
 69          handler.path = "/other/path"
 70          
 71          with patch.object(DocsHttpRequestHandler.__bases__[0], 'do_GET'):
 72              handler.do_GET()
 73          
 74          assert handler.path == "/other/path"
 75      
 76      def test_404_redirect(self):
 77          """Test that 404 errors redirect to introduction page"""
 78          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
 79          
 80          # Mock the response methods
 81          handler.send_response = Mock()
 82          handler.send_header = Mock()
 83          handler.end_headers = Mock()
 84          
 85          handler.send_error(404)
 86          
 87          handler.send_response.assert_called_once_with(302)
 88          handler.send_header.assert_called_once_with(
 89              'Location',
 90              '/solace-agent-mesh/docs/documentation/getting-started/introduction/'
 91          )
 92          handler.end_headers.assert_called_once()
 93      
 94      def test_non_404_error_uses_parent(self):
 95          """Test that non-404 errors use parent error handling"""
 96          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
 97          
 98          with patch.object(DocsHttpRequestHandler.__bases__[0], 'send_error') as mock_parent:
 99              handler.send_error(500, "Internal Server Error")
100              mock_parent.assert_called_once_with(500, "Internal Server Error")
101  
102  
103  class TestDocsCommand:
104      """Tests for the docs CLI command"""
105      
106      def test_docs_command_with_prod_docs(self, runner, tmp_path, mocker):
107          """Test docs command when production docs directory exists"""
108          # Create mock prod docs directory
109          prod_docs = tmp_path / "assets" / "docs"
110          prod_docs.mkdir(parents=True)
111          (prod_docs / "index.html").write_text("<html>Prod Docs</html>")
112          
113          # Mock get_cli_root_dir to return our tmp_path
114          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path))
115          
116          # Mock webbrowser and TCPServer
117          mock_browser = mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
118          mock_server = MagicMock()
119          mock_server.__enter__ = Mock(return_value=mock_server)
120          mock_server.__exit__ = Mock(return_value=False)
121          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
122          
123          mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
124          
125          result = runner.invoke(docs)
126          
127          assert result.exit_code == 0
128          assert "Starting documentation server" in result.output
129          mock_browser.assert_called_once()
130          assert "http://localhost:8585" in mock_browser.call_args[0][0]
131      
132      def test_docs_command_with_dev_docs(self, runner, tmp_path, mocker):
133          """Test docs command when only dev docs directory exists"""
134          # Create mock dev docs directory
135          dev_docs = tmp_path / "docs" / "build"
136          dev_docs.mkdir(parents=True)
137          (dev_docs / "index.html").write_text("<html>Dev Docs</html>")
138          
139          # Mock paths - prod doesn't exist, dev does
140          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path / "nonexistent"))
141          mocker.patch("cli.commands.docs_cmd.os.path.dirname", return_value=str(tmp_path))
142          
143          def mock_exists(path):
144              return "docs/build" in str(path)
145          
146          mocker.patch("cli.commands.docs_cmd.os.path.exists", side_effect=mock_exists)
147          
148          # Mock webbrowser and TCPServer
149          mock_browser = mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
150          mock_server = MagicMock()
151          mock_server.__enter__ = Mock(return_value=mock_server)
152          mock_server.__exit__ = Mock(return_value=False)
153          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
154          
155          mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
156          
157          result = runner.invoke(docs)
158          
159          assert result.exit_code == 0
160          assert "Serving development documentation" in result.output
161          mock_browser.assert_called_once()
162      
163      def test_docs_command_custom_port(self, runner, tmp_path, mocker):
164          """Test docs command with custom port"""
165          # Create mock docs directory
166          prod_docs = tmp_path / "assets" / "docs"
167          prod_docs.mkdir(parents=True)
168          
169          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path))
170          
171          # Mock webbrowser and TCPServer
172          mock_browser = mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
173          mock_server = MagicMock()
174          mock_server.__enter__ = Mock(return_value=mock_server)
175          mock_server.__exit__ = Mock(return_value=False)
176          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
177          
178          mock_tcp_server = mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
179          
180          result = runner.invoke(docs, ["--port", "9000"])
181          
182          assert result.exit_code == 0
183          assert "http://localhost:9000" in result.output
184          mock_browser.assert_called_once()
185          assert "http://localhost:9000" in mock_browser.call_args[0][0]
186          
187          # Verify TCPServer was called with correct port
188          assert mock_tcp_server.call_args[0][0] == ("", 9000)
189      
190      def test_docs_command_missing_docs_directory(self, runner, tmp_path, mocker):
191          """Test docs command when no docs directory exists"""
192          # Mock paths to non-existent directories
193          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path / "nonexistent"))
194          mocker.patch("cli.commands.docs_cmd.os.path.dirname", return_value=str(tmp_path / "nonexistent"))
195          mocker.patch("cli.commands.docs_cmd.os.path.exists", return_value=False)
196          
197          # Mock error_exit to raise SystemExit
198          mock_error_exit = mocker.patch("cli.commands.docs_cmd.error_exit", side_effect=SystemExit(1))
199          
200          result = runner.invoke(docs)
201          
202          assert result.exit_code == 1
203          mock_error_exit.assert_called_once()
204          assert "Documentation directory not found" in mock_error_exit.call_args[0][0]
205      
206      def test_docs_command_keyboard_interrupt(self, runner, tmp_path, mocker):
207          """Test docs command handles keyboard interrupt gracefully"""
208          # Create mock docs directory
209          prod_docs = tmp_path / "assets" / "docs"
210          prod_docs.mkdir(parents=True)
211          
212          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path))
213          mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
214          
215          # Mock TCPServer to raise KeyboardInterrupt
216          mock_server = MagicMock()
217          mock_server.__enter__ = Mock(return_value=mock_server)
218          mock_server.__exit__ = Mock(return_value=False)
219          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
220          
221          mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
222          
223          result = runner.invoke(docs)
224          
225          # Should exit cleanly on KeyboardInterrupt
226          assert result.exit_code == 0
227      
228      def test_docs_command_port_option_validation(self, runner, tmp_path, mocker):
229          """Test docs command validates port as integer"""
230          result = runner.invoke(docs, ["--port", "invalid"])
231          
232          assert result.exit_code != 0
233          assert "Invalid value" in result.output or "Error" in result.output
234      
235      def test_docs_command_short_port_option(self, runner, tmp_path, mocker):
236          """Test docs command with short -p option"""
237          # Create mock docs directory
238          prod_docs = tmp_path / "assets" / "docs"
239          prod_docs.mkdir(parents=True)
240          
241          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path))
242          
243          mock_browser = mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
244          mock_server = MagicMock()
245          mock_server.__enter__ = Mock(return_value=mock_server)
246          mock_server.__exit__ = Mock(return_value=False)
247          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
248          
249          mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
250          
251          result = runner.invoke(docs, ["-p", "7777"])
252          
253          assert result.exit_code == 0
254          assert "http://localhost:7777" in result.output
255      
256      def test_docs_handler_initialization(self, tmp_path):
257          """Test DocsHttpRequestHandler initialization with directory"""
258          docs_dir = str(tmp_path / "docs")
259          
260          # Test that the handler class can be created with directory parameter
261          # We use __new__ to avoid triggering the socket handling in __init__
262          handler = DocsHttpRequestHandler.__new__(DocsHttpRequestHandler)
263          
264          # Verify the handler class was created (basic smoke test)
265          assert handler is not None
266          assert isinstance(handler, DocsHttpRequestHandler)
267      
268      def test_docs_command_url_format(self, runner, tmp_path, mocker):
269          """Test that the URL format is correct"""
270          prod_docs = tmp_path / "assets" / "docs"
271          prod_docs.mkdir(parents=True)
272          
273          mocker.patch("cli.commands.docs_cmd.get_cli_root_dir", return_value=str(tmp_path))
274          
275          mock_browser = mocker.patch("cli.commands.docs_cmd.webbrowser.open_new_tab")
276          mock_server = MagicMock()
277          mock_server.__enter__ = Mock(return_value=mock_server)
278          mock_server.__exit__ = Mock(return_value=False)
279          mock_server.serve_forever = Mock(side_effect=KeyboardInterrupt)
280          
281          mocker.patch("cli.commands.docs_cmd.socketserver.TCPServer", return_value=mock_server)
282          
283          result = runner.invoke(docs, ["--port", "8585"])
284          
285          # Verify the exact URL format
286          expected_url = "http://localhost:8585/solace-agent-mesh/docs/documentation/getting-started/introduction/"
287          mock_browser.assert_called_once_with(expected_url)
288          assert expected_url in result.output