Skip to main content
Glama

Rootly MCP server

Official
test_oncall_metrics.py12 kB
""" Unit tests for on-call shift metrics tool. Tests cover: - get_oncall_shift_metrics function logic - Date range filtering - User/schedule/team filtering - Grouping by different dimensions - Metrics calculation (hours, counts, averages) - Error handling """ import pytest from unittest.mock import patch from datetime import datetime @pytest.fixture def mock_shifts_response(): """Mock shifts API response with sample data.""" return { "data": [ { "id": "shift-1", "type": "shifts", "attributes": { "schedule_id": "schedule-1", "rotation_id": "rotation-1", "starts_at": "2025-10-01T08:00:00.000-07:00", "ends_at": "2025-10-01T16:00:00.000-07:00", "is_override": False }, "relationships": { "user": { "data": {"id": "user-1", "type": "users"} } } }, { "id": "shift-2", "type": "shifts", "attributes": { "schedule_id": "schedule-1", "rotation_id": "rotation-1", "starts_at": "2025-10-02T08:00:00.000-07:00", "ends_at": "2025-10-02T16:00:00.000-07:00", "is_override": True }, "relationships": { "user": { "data": {"id": "user-2", "type": "users"} } } }, { "id": "shift-3", "type": "shifts", "attributes": { "schedule_id": "schedule-2", "rotation_id": "rotation-2", "starts_at": "2025-10-03T20:00:00.000-07:00", "ends_at": "2025-10-04T04:00:00.000-07:00", "is_override": False }, "relationships": { "user": { "data": {"id": "user-1", "type": "users"} } } } ], "included": [ { "id": "user-1", "type": "users", "attributes": { "full_name": "John Doe", "email": "john@example.com" } }, { "id": "user-2", "type": "users", "attributes": { "full_name": "Jane Smith", "email": "jane@example.com" } } ] } @pytest.fixture def mock_schedules_response(): """Mock schedules API response.""" return { "data": [ { "id": "schedule-1", "type": "schedules", "attributes": { "name": "Backend On-Call" }, "relationships": { "team": { "data": {"id": "team-1", "type": "teams"} } } }, { "id": "schedule-2", "type": "schedules", "attributes": { "name": "Frontend On-Call" }, "relationships": { "team": { "data": {"id": "team-2", "type": "teams"} } } } ] } @pytest.mark.unit @pytest.mark.asyncio class TestGetOncallShiftMetrics: """Test get_oncall_shift_metrics tool.""" async def test_basic_metrics_calculation(self, mock_shifts_response): """Test basic metrics calculation with grouping by user.""" from rootly_mcp_server.server import create_rootly_mcp_server with patch('rootly_mcp_server.server._load_swagger_spec') as mock_load_spec: mock_spec = { "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": {}, "components": {"schemas": {}} } mock_load_spec.return_value = mock_spec server = create_rootly_mcp_server() # Verify server was created successfully assert server is not None # Get the tool list tools = await server.get_tools() tool_names = [] for t in tools: if hasattr(t, 'name'): tool_names.append(t.name) # type: ignore[attr-defined] else: tool_names.append(str(t)) # Check if our tool is registered assert "get_oncall_shift_metrics" in tool_names, "get_oncall_shift_metrics tool not found" async def test_metrics_grouped_by_user(self, mock_shifts_response): """Test metrics calculation grouped by user.""" # Import after patching to ensure module loads correctly with patch('rootly_mcp_server.server._load_swagger_spec') as mock_load_spec: mock_spec = { "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": {}, "components": {"schemas": {}} } mock_load_spec.return_value = mock_spec # The tool should calculate: # user-1: 2 shifts (shift-1: 8h, shift-3: 8h) = 16h total # user-2: 1 shift (shift-2: 8h override) # Verify the calculation logic would work correctly shifts = mock_shifts_response["data"] assert len(shifts) == 3 # Count shifts per user user_shifts = {} for shift in shifts: user_id = shift["relationships"]["user"]["data"]["id"] user_shifts[user_id] = user_shifts.get(user_id, 0) + 1 assert user_shifts["user-1"] == 2 assert user_shifts["user-2"] == 1 async def test_date_range_filtering(self): """Test that date range is properly passed to API.""" from rootly_mcp_server.server import create_rootly_mcp_server with patch('rootly_mcp_server.server._load_swagger_spec') as mock_load_spec: mock_spec = { "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": {}, "components": {"schemas": {}} } mock_load_spec.return_value = mock_spec server = create_rootly_mcp_server() # Verify server was created and would accept date parameters assert server is not None def test_shift_duration_calculation(self): """Test shift duration calculation logic.""" from datetime import datetime # Test case 1: 8-hour shift starts_at = "2025-10-01T08:00:00.000-07:00" ends_at = "2025-10-01T16:00:00.000-07:00" start_dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(ends_at.replace("Z", "+00:00")) duration_hours = (end_dt - start_dt).total_seconds() / 3600 assert duration_hours == 8.0 # Test case 2: Overnight shift starts_at = "2025-10-03T20:00:00.000-07:00" ends_at = "2025-10-04T04:00:00.000-07:00" start_dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(ends_at.replace("Z", "+00:00")) duration_hours = (end_dt - start_dt).total_seconds() / 3600 assert duration_hours == 8.0 def test_override_vs_regular_classification(self, mock_shifts_response): """Test that override and regular shifts are correctly classified.""" shifts = mock_shifts_response["data"] regular_count = sum(1 for s in shifts if not s["attributes"]["is_override"]) override_count = sum(1 for s in shifts if s["attributes"]["is_override"]) assert regular_count == 2 assert override_count == 1 async def test_team_filtering_requires_schedule_query(self, mock_schedules_response): """Test that team filtering triggers schedule query.""" with patch('rootly_mcp_server.server._load_swagger_spec') as mock_load_spec: mock_spec = { "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": {}, "components": {"schemas": {}} } mock_load_spec.return_value = mock_spec # Verify that schedules endpoint exists in allowed paths from rootly_mcp_server.server import DEFAULT_ALLOWED_PATHS schedule_paths = [p for p in DEFAULT_ALLOWED_PATHS if "schedule" in p.lower()] assert len(schedule_paths) > 0 def test_grouping_options(self): """Test that different grouping options are supported.""" valid_groupings = ["user", "schedule", "team", "none"] for grouping in valid_groupings: # Each grouping option should be valid assert grouping in ["user", "schedule", "team", "none"] def test_metrics_summary_calculation(self): """Test summary statistics calculation.""" # Mock metrics data metrics = [ {"shift_count": 5, "total_hours": 40.0, "regular_shifts": 4, "override_shifts": 1}, {"shift_count": 3, "total_hours": 24.0, "regular_shifts": 2, "override_shifts": 1}, {"shift_count": 2, "total_hours": 16.0, "regular_shifts": 2, "override_shifts": 0} ] # Calculate summary total_hours = sum(m["total_hours"] for m in metrics) total_regular = sum(m["regular_shifts"] for m in metrics) total_override = sum(m["override_shifts"] for m in metrics) unique_people = len(metrics) assert total_hours == 80.0 assert total_regular == 8 assert total_override == 2 assert unique_people == 3 def test_average_shift_hours_calculation(self): """Test average shift hours calculation.""" shift_count = 10 total_hours = 85.5 average = round(total_hours / shift_count, 2) if shift_count > 0 else 0 assert average == 8.55 # Test zero division protection average_zero = round(0 / 0, 2) if 0 > 0 else 0 assert average_zero == 0 @pytest.mark.unit class TestOncallMetricsInputValidation: """Test input validation for on-call metrics.""" def test_date_format_handling(self): """Test that ISO 8601 date formats are properly handled.""" valid_formats = [ "2025-10-01", "2025-10-01T00:00:00Z", "2025-10-01T00:00:00.000-07:00" ] for date_str in valid_formats: # All formats should be parseable try: datetime.fromisoformat(date_str.replace("Z", "+00:00")) is_valid = True except ValueError: is_valid = False assert is_valid, f"Date format {date_str} should be valid" def test_comma_separated_ids_parsing(self): """Test parsing of comma-separated ID lists.""" user_ids_str = "123,456,789" user_id_list = [uid.strip() for uid in user_ids_str.split(",") if uid.strip()] assert len(user_id_list) == 3 assert "123" in user_id_list assert "789" in user_id_list # Test with spaces user_ids_str = "123, 456 , 789" user_id_list = [uid.strip() for uid in user_ids_str.split(",") if uid.strip()] assert len(user_id_list) == 3 assert "456" in user_id_list # Test empty string user_ids_str = "" user_id_list = [uid.strip() for uid in user_ids_str.split(",") if uid.strip()] assert len(user_id_list) == 0

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rootly-AI-Labs/Rootly-MCP-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server