Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit cd7a3b2

Browse files
authored
Merge pull request #307 from stacklok/fix_uvicorn_tests
fix: readd tests that were added at cli command
2 parents a598168 + 7c4e0c9 commit cd7a3b2

File tree

1 file changed

+353
-1
lines changed

1 file changed

+353
-1
lines changed

tests/test_cli.py

Lines changed: 353 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for the server module."""
22

3-
from unittest.mock import MagicMock, patch
3+
import os
4+
from unittest.mock import MagicMock, patch, AsyncMock
45

56
import pytest
67
from fastapi.middleware.cors import CORSMiddleware
@@ -12,6 +13,12 @@
1213
from codegate.pipeline.secrets.manager import SecretsManager
1314
from codegate.providers.registry import ProviderRegistry
1415
from codegate.server import init_app
16+
from src.codegate.cli import UvicornServer
17+
from src.codegate.cli import cli
18+
from src.codegate.codegate_logging import LogLevel, LogFormat
19+
from uvicorn.config import Config as UvicornConfig
20+
from click.testing import CliRunner
21+
from pathlib import Path
1522

1623

1724
@pytest.fixture
@@ -158,3 +165,348 @@ def test_error_handling(test_client: TestClient) -> None:
158165
# Test method not allowed
159166
response = test_client.post("/health") # Health endpoint only allows GET
160167
assert response.status_code == 405
168+
169+
170+
@pytest.fixture
171+
def mock_app():
172+
# Create a simple mock for the ASGI application
173+
return MagicMock()
174+
175+
176+
@pytest.fixture
177+
def uvicorn_config(mock_app):
178+
# Assuming mock_app is defined to simulate ASGI application
179+
return UvicornConfig(app=mock_app, host='localhost', port=8000, log_level='info')
180+
181+
182+
@pytest.fixture
183+
def server_instance(uvicorn_config):
184+
with patch('src.codegate.cli.Server', autospec=True) as mock_server_class:
185+
mock_server_instance = mock_server_class.return_value
186+
mock_server_instance.serve = AsyncMock()
187+
yield UvicornServer(uvicorn_config, mock_server_instance)
188+
189+
190+
@pytest.mark.asyncio
191+
async def test_server_starts_and_runs(server_instance):
192+
await server_instance.serve()
193+
server_instance.server.serve.assert_awaited_once()
194+
195+
196+
@pytest.fixture
197+
def cli_runner():
198+
return CliRunner()
199+
200+
201+
@pytest.fixture
202+
def mock_logging(mocker):
203+
return mocker.patch('your_cli_module.structlog.get_logger')
204+
205+
206+
@pytest.fixture
207+
def mock_setup_logging(mocker):
208+
return mocker.patch('your_cli_module.setup_logging')
209+
210+
211+
def test_serve_default_options(cli_runner):
212+
"""Test serve command with default options."""
213+
# Use patches for run_servers and logging setup
214+
with patch("src.codegate.cli.run_servers") as mock_run, \
215+
patch("src.codegate.cli.structlog.get_logger") as mock_logging, \
216+
patch("src.codegate.cli.setup_logging") as mock_setup_logging:
217+
218+
logger_instance = MagicMock()
219+
mock_logging.return_value = logger_instance
220+
221+
# Invoke the CLI command
222+
result = cli_runner.invoke(cli, ["serve"])
223+
224+
# Basic checks to ensure the command executed successfully
225+
assert result.exit_code == 0
226+
227+
# Check if the logging setup was called with expected defaults
228+
mock_setup_logging.assert_called_once_with(LogLevel.INFO, LogFormat.JSON)
229+
230+
# Check if logging was done correctly
231+
mock_logging.assert_called_with("codegate")
232+
233+
# Validate run_servers was called once
234+
mock_run.assert_called_once()
235+
236+
237+
def test_serve_custom_options(cli_runner):
238+
"""Test serve command with custom options."""
239+
with patch("src.codegate.cli.run_servers") as mock_run, \
240+
patch("src.codegate.cli.structlog.get_logger") as mock_logging, \
241+
patch("src.codegate.cli.setup_logging") as mock_setup_logging:
242+
243+
logger_instance = MagicMock()
244+
mock_logging.return_value = logger_instance
245+
246+
# Invoke the CLI command with custom options
247+
result = cli_runner.invoke(
248+
cli,
249+
[
250+
"serve",
251+
"--port", "8989",
252+
"--host", "localhost",
253+
"--log-level", "DEBUG",
254+
"--log-format", "TEXT",
255+
"--certs-dir", "./custom-certs",
256+
"--ca-cert", "custom-ca.crt",
257+
"--ca-key", "custom-ca.key",
258+
"--server-cert", "custom-server.crt",
259+
"--server-key", "custom-server.key",
260+
],
261+
)
262+
263+
# Check the command executed successfully
264+
assert result.exit_code == 0
265+
266+
# Assert logging setup was called with the provided log level and format
267+
mock_setup_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.TEXT)
268+
269+
# Assert logger got called with the expected module name
270+
mock_logging.assert_called_with("codegate")
271+
272+
# Validate run_servers was called once
273+
mock_run.assert_called_once()
274+
# Retrieve the actual Config object passed to run_servers
275+
config_arg = mock_run.call_args[0][0] # Assuming Config is the first positional arg
276+
277+
# Define expected values that should be present in the Config object
278+
expected_values = {
279+
"port": 8989,
280+
"host": "localhost",
281+
"log_level": LogLevel.DEBUG,
282+
"log_format": LogFormat.TEXT,
283+
"certs_dir": "./custom-certs",
284+
"ca_cert": "custom-ca.crt",
285+
"ca_key": "custom-ca.key",
286+
"server_cert": "custom-server.crt",
287+
"server_key": "custom-server.key",
288+
}
289+
290+
# Check if Config object attributes match the expected values
291+
for key, expected_value in expected_values.items():
292+
assert getattr(config_arg, key) == expected_value, \
293+
f"{key} does not match expected value"
294+
295+
296+
def test_serve_invalid_port(cli_runner):
297+
"""Test serve command with invalid port."""
298+
result = cli_runner.invoke(cli, ["serve", "--port", "999999"])
299+
assert result.exit_code == 2 # Typically 2 is used for CLI errors in Click
300+
assert "Port must be between 1 and 65535" in result.output
301+
302+
303+
def test_serve_invalid_log_level(cli_runner):
304+
"""Test serve command with invalid log level."""
305+
result = cli_runner.invoke(cli, ["serve", "--log-level", "INVALID"])
306+
assert result.exit_code == 2
307+
assert "Invalid value for '--log-level'" in result.output
308+
309+
310+
@pytest.fixture
311+
def temp_config_file(tmp_path):
312+
config_path = tmp_path / "config.yaml"
313+
config_path.write_text("""
314+
log_level: DEBUG
315+
log_format: JSON
316+
port: 8989
317+
host: localhost
318+
certs_dir: ./test-certs
319+
""")
320+
return config_path
321+
322+
323+
def test_serve_with_config_file(cli_runner, temp_config_file):
324+
"""Test serve command with config file."""
325+
with patch("src.codegate.cli.run_servers") as mock_run, \
326+
patch("src.codegate.cli.structlog.get_logger") as mock_logging, \
327+
patch("src.codegate.cli.setup_logging") as mock_setup_logging:
328+
329+
logger_instance = MagicMock()
330+
mock_logging.return_value = logger_instance
331+
332+
# Invoke the CLI command with the configuration file
333+
result = cli_runner.invoke(cli, ["serve", "--config", str(temp_config_file)])
334+
335+
# Assertions to ensure the CLI ran successfully
336+
assert result.exit_code == 0
337+
mock_setup_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.JSON)
338+
mock_logging.assert_called_with("codegate")
339+
340+
# Validate that run_servers was called with the expected configuration
341+
mock_run.assert_called_once()
342+
config_arg = mock_run.call_args[0][0]
343+
344+
# Define expected values based on the temp_config_file content
345+
expected_values = {
346+
"port": 8989,
347+
"host": "localhost",
348+
"log_level": LogLevel.DEBUG,
349+
"log_format": LogFormat.JSON,
350+
"certs_dir": "./test-certs",
351+
}
352+
353+
# Check if passed arguments match the expected values
354+
for key, expected_value in expected_values.items():
355+
assert getattr(config_arg, key) == expected_value, \
356+
f"{key} does not match expected value"
357+
358+
359+
def test_serve_with_nonexistent_config_file(cli_runner: CliRunner) -> None:
360+
"""Test serve command with nonexistent config file."""
361+
result = cli_runner.invoke(cli, ["serve", "--config", "nonexistent.yaml"])
362+
assert result.exit_code == 2
363+
assert "does not exist" in result.output
364+
365+
366+
def test_serve_priority_resolution(cli_runner: CliRunner, temp_config_file: Path) -> None:
367+
"""Test serve command respects configuration priority."""
368+
# Set up environment variables and ensure they get cleaned up after the test
369+
with patch.dict(os.environ, {'LOG_LEVEL': 'INFO', 'PORT': '9999'}, clear=True), \
370+
patch('src.codegate.cli.run_servers') as mock_run, \
371+
patch('src.codegate.cli.structlog.get_logger') as mock_logging, \
372+
patch('src.codegate.cli.setup_logging') as mock_setup_logging:
373+
# Set up mock logger
374+
logger_instance = MagicMock()
375+
mock_logging.return_value = logger_instance
376+
377+
# Execute CLI command with specific options overriding environment and config file settings
378+
result = cli_runner.invoke(
379+
cli,
380+
[
381+
"serve",
382+
"--config",
383+
str(temp_config_file),
384+
"--port",
385+
"8080",
386+
"--host",
387+
"example.com",
388+
"--log-level",
389+
"ERROR",
390+
"--log-format",
391+
"TEXT",
392+
"--certs-dir",
393+
"./cli-certs",
394+
"--ca-cert",
395+
"cli-ca.crt",
396+
"--ca-key",
397+
"cli-ca.key",
398+
"--server-cert",
399+
"cli-server.crt",
400+
"--server-key",
401+
"cli-server.key",
402+
],
403+
)
404+
405+
# Check the result of the command
406+
assert result.exit_code == 0
407+
408+
# Ensure logging setup was called with the highest priority settings (CLI arguments)
409+
mock_setup_logging.assert_called_once_with('ERROR', 'TEXT')
410+
mock_logging.assert_called_with("codegate")
411+
412+
# Verify that the run_servers was called with the overridden settings
413+
config_arg = mock_run.call_args[0][0] # Assuming Config is the first positional arg
414+
415+
expected_values = {
416+
"port": 8080,
417+
"host": "example.com",
418+
"log_level": 'ERROR',
419+
"log_format": 'TEXT',
420+
"certs_dir": "./cli-certs",
421+
"ca_cert": "cli-ca.crt",
422+
"ca_key": "cli-ca.key",
423+
"server_cert": "cli-server.crt",
424+
"server_key": "cli-server.key",
425+
}
426+
427+
# Verify if Config object attributes match the expected values from CLI arguments
428+
for key, expected_value in expected_values.items():
429+
assert getattr(config_arg, key) == expected_value, \
430+
f"{key} does not match expected value"
431+
432+
433+
def test_serve_certificate_options(cli_runner: CliRunner) -> None:
434+
"""Test serve command with certificate options."""
435+
with patch('src.codegate.cli.run_servers') as mock_run, \
436+
patch('src.codegate.cli.structlog.get_logger') as mock_logging, \
437+
patch('src.codegate.cli.setup_logging') as mock_setup_logging:
438+
# Set up mock logger
439+
logger_instance = MagicMock()
440+
mock_logging.return_value = logger_instance
441+
442+
# Execute CLI command with certificate options
443+
result = cli_runner.invoke(
444+
cli,
445+
[
446+
"serve",
447+
"--certs-dir",
448+
"./custom-certs",
449+
"--ca-cert",
450+
"custom-ca.crt",
451+
"--ca-key",
452+
"custom-ca.key",
453+
"--server-cert",
454+
"custom-server.crt",
455+
"--server-key",
456+
"custom-server.key",
457+
],
458+
)
459+
460+
# Check the result of the command
461+
assert result.exit_code == 0
462+
463+
# Ensure logging setup was called with expected arguments
464+
mock_setup_logging.assert_called_once_with('INFO', 'JSON')
465+
mock_logging.assert_called_with("codegate")
466+
467+
# Verify that run_servers was called with the provided certificate options
468+
config_arg = mock_run.call_args[0][0] # Assuming Config is the first positional arg
469+
470+
expected_values = {
471+
"certs_dir": "./custom-certs",
472+
"ca_cert": "custom-ca.crt",
473+
"ca_key": "custom-ca.key",
474+
"server_cert": "custom-server.crt",
475+
"server_key": "custom-server.key",
476+
}
477+
478+
# Check if Config object attributes match the expected values
479+
for key, expected_value in expected_values.items():
480+
assert getattr(config_arg, key) == expected_value, \
481+
f"{key} does not match expected value"
482+
483+
484+
def test_main_function() -> None:
485+
"""Test main function."""
486+
with patch("sys.argv", ["cli"]), patch("codegate.cli.cli") as mock_cli:
487+
from codegate.cli import main
488+
main()
489+
mock_cli.assert_called_once()
490+
491+
492+
@pytest.fixture
493+
def mock_uvicorn_server():
494+
mock_config = MagicMock() # Setup the configuration mock
495+
mock_server = MagicMock(spec=UvicornServer)
496+
mock_server.shutdown = AsyncMock() # Ensure shutdown is an async mock
497+
498+
uvicorn_server = UvicornServer(config=mock_config, server=mock_server)
499+
return uvicorn_server
500+
501+
502+
@pytest.mark.asyncio
503+
async def test_uvicorn_server_cleanup(mock_uvicorn_server):
504+
with patch("asyncio.get_running_loop"), \
505+
patch.object(mock_uvicorn_server.server, 'shutdown', AsyncMock()):
506+
# Mock the loop or other components as needed
507+
508+
# Start the server or trigger the condition you want to test
509+
await mock_uvicorn_server.cleanup() # This should now complete without error
510+
511+
# Verify that the shutdown was called
512+
mock_uvicorn_server.server.shutdown.assert_awaited_once()

0 commit comments

Comments
 (0)