ServiceNow MCP Server
by osomai
Verified
- servicenow-mcp
- tests
"""
Tests for the ServiceNow MCP catalog optimization tools.
"""
import unittest
from unittest.mock import MagicMock, patch
import requests
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.catalog_optimization import (
OptimizationRecommendationsParams,
UpdateCatalogItemParams,
_get_high_abandonment_items,
_get_inactive_items,
_get_low_usage_items,
_get_poor_description_items,
_get_slow_fulfillment_items,
get_optimization_recommendations,
update_catalog_item,
)
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
class TestCatalogOptimizationTools(unittest.TestCase):
"""Test cases for the catalog optimization tools."""
def setUp(self):
"""Set up test fixtures."""
# Create a mock server config
self.config = ServerConfig(
instance_url="https://example.service-now.com",
auth=AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username="admin", password="password"),
),
)
# Create a mock auth manager
self.auth_manager = MagicMock(spec=AuthManager)
self.auth_manager.get_headers.return_value = {"Authorization": "Basic YWRtaW46cGFzc3dvcmQ="}
@patch("requests.get")
def test_get_inactive_items(self, mock_get):
"""Test getting inactive catalog items."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": [
{
"sys_id": "item1",
"name": "Old Laptop",
"short_description": "Outdated laptop model",
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Legacy Software",
"short_description": "Deprecated software package",
"category": "software",
},
]
}
mock_get.return_value = mock_response
# Call the function
result = _get_inactive_items(self.config, self.auth_manager)
# Verify the results
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["name"], "Old Laptop")
self.assertEqual(result[1]["name"], "Legacy Software")
# Verify the API call
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
self.assertEqual(kwargs["params"]["sysparm_query"], "active=false")
@patch("requests.get")
def test_get_inactive_items_with_category(self, mock_get):
"""Test getting inactive catalog items filtered by category."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": [
{
"sys_id": "item1",
"name": "Old Laptop",
"short_description": "Outdated laptop model",
"category": "hardware",
},
]
}
mock_get.return_value = mock_response
# Call the function with a category filter
result = _get_inactive_items(self.config, self.auth_manager, "hardware")
# Verify the results
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["name"], "Old Laptop")
# Verify the API call
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
self.assertEqual(kwargs["params"]["sysparm_query"], "active=false^category=hardware")
@patch("requests.get")
def test_get_inactive_items_error(self, mock_get):
"""Test error handling when getting inactive catalog items."""
# Mock an error response
mock_get.side_effect = requests.exceptions.RequestException("API Error")
# Call the function
result = _get_inactive_items(self.config, self.auth_manager)
# Verify the results
self.assertEqual(result, [])
@patch("requests.get")
@patch("random.sample")
@patch("random.randint")
def test_get_low_usage_items(self, mock_randint, mock_sample, mock_get):
"""Test getting catalog items with low usage."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": [
{
"sys_id": "item1",
"name": "Rarely Used Laptop",
"short_description": "Laptop model with low demand",
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Unpopular Software",
"short_description": "Software with few users",
"category": "software",
},
{
"sys_id": "item3",
"name": "Niche Service",
"short_description": "Specialized service with limited audience",
"category": "services",
},
]
}
mock_get.return_value = mock_response
# Mock the random sample to return the first two items
mock_sample.return_value = [
{
"sys_id": "item1",
"name": "Rarely Used Laptop",
"short_description": "Laptop model with low demand",
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Unpopular Software",
"short_description": "Software with few users",
"category": "software",
},
]
# Mock the random order counts
mock_randint.return_value = 2
# Call the function
result = _get_low_usage_items(self.config, self.auth_manager)
# Verify the results
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["name"], "Rarely Used Laptop")
self.assertEqual(result[0]["order_count"], 2)
self.assertEqual(result[1]["name"], "Unpopular Software")
self.assertEqual(result[1]["order_count"], 2)
# Verify the API call
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
self.assertEqual(kwargs["params"]["sysparm_query"], "active=true")
def test_high_abandonment_items_format(self):
"""Test the expected format of high abandonment items."""
# This test doesn't call the actual function, but verifies the expected format
# of the data that would be returned by the function
# Example data that would be returned by _get_high_abandonment_items
high_abandonment_items = [
{
"sys_id": "item1",
"name": "Complex Request",
"short_description": "Request with many fields",
"category": "hardware",
"abandonment_rate": 60,
"cart_adds": 30,
"orders": 12,
},
{
"sys_id": "item2",
"name": "Expensive Item",
"short_description": "High-cost item",
"category": "software",
"abandonment_rate": 60,
"cart_adds": 20,
"orders": 8,
},
]
# Verify the expected format
self.assertEqual(len(high_abandonment_items), 2)
self.assertEqual(high_abandonment_items[0]["name"], "Complex Request")
self.assertEqual(high_abandonment_items[0]["abandonment_rate"], 60)
self.assertEqual(high_abandonment_items[0]["cart_adds"], 30)
self.assertEqual(high_abandonment_items[0]["orders"], 12)
self.assertEqual(high_abandonment_items[1]["name"], "Expensive Item")
self.assertEqual(high_abandonment_items[1]["abandonment_rate"], 60)
self.assertEqual(high_abandonment_items[1]["cart_adds"], 20)
self.assertEqual(high_abandonment_items[1]["orders"], 8)
@patch("requests.get")
@patch("random.sample")
@patch("random.uniform")
def test_get_slow_fulfillment_items(self, mock_uniform, mock_sample, mock_get):
"""Test getting catalog items with slow fulfillment times."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": [
{
"sys_id": "item1",
"name": "Custom Hardware",
"short_description": "Specialized hardware request",
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Complex Software",
"short_description": "Software with complex installation",
"category": "software",
},
]
}
mock_get.return_value = mock_response
# Mock the random sample to return all items
mock_sample.return_value = [
{
"sys_id": "item1",
"name": "Custom Hardware",
"short_description": "Specialized hardware request",
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Complex Software",
"short_description": "Software with complex installation",
"category": "software",
},
]
# Mock the random uniform values for fulfillment times
mock_uniform.return_value = 7.5
# Call the function
result = _get_slow_fulfillment_items(self.config, self.auth_manager)
# Verify the results
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["name"], "Custom Hardware")
self.assertEqual(result[0]["avg_fulfillment_time"], 7.5)
self.assertEqual(result[0]["avg_fulfillment_time_vs_catalog"], 3.0) # 7.5 / 2.5 = 3.0
self.assertEqual(result[1]["name"], "Complex Software")
self.assertEqual(result[1]["avg_fulfillment_time"], 7.5)
self.assertEqual(result[1]["avg_fulfillment_time_vs_catalog"], 3.0) # 7.5 / 2.5 = 3.0
@patch("requests.get")
def test_get_poor_description_items(self, mock_get):
"""Test getting catalog items with poor description quality."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": [
{
"sys_id": "item1",
"name": "Laptop",
"short_description": "", # Empty description
"category": "hardware",
},
{
"sys_id": "item2",
"name": "Software",
"short_description": "Software package", # Short description
"category": "software",
},
{
"sys_id": "item3",
"name": "Service",
"short_description": "Please click here to request this service", # Instructional language
"category": "services",
},
]
}
mock_get.return_value = mock_response
# Call the function
result = _get_poor_description_items(self.config, self.auth_manager)
# Verify the results
self.assertEqual(len(result), 3)
# Check the first item (empty description)
self.assertEqual(result[0]["name"], "Laptop")
self.assertEqual(result[0]["description_quality"], 0)
self.assertEqual(result[0]["quality_issues"], ["Missing description"])
# Check the second item (short description)
self.assertEqual(result[1]["name"], "Software")
self.assertEqual(result[1]["description_quality"], 30)
self.assertEqual(result[1]["quality_issues"], ["Description too short", "Lacks detail"])
# Check the third item (instructional language)
self.assertEqual(result[2]["name"], "Service")
self.assertEqual(result[2]["description_quality"], 50)
self.assertEqual(result[2]["quality_issues"], ["Uses instructional language instead of descriptive"])
@patch("servicenow_mcp.tools.catalog_optimization._get_inactive_items")
@patch("servicenow_mcp.tools.catalog_optimization._get_low_usage_items")
@patch("servicenow_mcp.tools.catalog_optimization._get_high_abandonment_items")
@patch("servicenow_mcp.tools.catalog_optimization._get_slow_fulfillment_items")
@patch("servicenow_mcp.tools.catalog_optimization._get_poor_description_items")
def test_get_optimization_recommendations(
self,
mock_poor_desc,
mock_slow_fulfill,
mock_high_abandon,
mock_low_usage,
mock_inactive
):
"""Test getting optimization recommendations."""
# Mock the helper functions to return test data
mock_inactive.return_value = [
{
"sys_id": "item1",
"name": "Old Laptop",
"short_description": "Outdated laptop model",
"category": "hardware",
},
]
mock_low_usage.return_value = [
{
"sys_id": "item2",
"name": "Rarely Used Software",
"short_description": "Software with few users",
"category": "software",
"order_count": 2,
},
]
mock_high_abandon.return_value = [
{
"sys_id": "item3",
"name": "Complex Request",
"short_description": "Request with many fields",
"category": "hardware",
"abandonment_rate": 60,
"cart_adds": 30,
"orders": 12,
},
]
mock_slow_fulfill.return_value = [
{
"sys_id": "item4",
"name": "Custom Hardware",
"short_description": "Specialized hardware request",
"category": "hardware",
"avg_fulfillment_time": 7.5,
"avg_fulfillment_time_vs_catalog": 3.0,
},
]
mock_poor_desc.return_value = [
{
"sys_id": "item5",
"name": "Laptop",
"short_description": "",
"category": "hardware",
"description_quality": 0,
"quality_issues": ["Missing description"],
},
]
# Create the parameters
params = OptimizationRecommendationsParams(
recommendation_types=[
"inactive_items",
"low_usage",
"high_abandonment",
"slow_fulfillment",
"description_quality"
]
)
# Call the function
result = get_optimization_recommendations(self.config, self.auth_manager, params)
# Verify the results
self.assertTrue(result["success"])
self.assertEqual(len(result["recommendations"]), 5)
# Check each recommendation type
recommendation_types = [rec["type"] for rec in result["recommendations"]]
self.assertIn("inactive_items", recommendation_types)
self.assertIn("low_usage", recommendation_types)
self.assertIn("high_abandonment", recommendation_types)
self.assertIn("slow_fulfillment", recommendation_types)
self.assertIn("description_quality", recommendation_types)
# Check that each recommendation has the expected fields
for rec in result["recommendations"]:
self.assertIn("title", rec)
self.assertIn("description", rec)
self.assertIn("items", rec)
self.assertIn("impact", rec)
self.assertIn("effort", rec)
self.assertIn("action", rec)
@patch("servicenow_mcp.tools.catalog_optimization._get_inactive_items")
@patch("servicenow_mcp.tools.catalog_optimization._get_low_usage_items")
def test_get_optimization_recommendations_filtered(self, mock_low_usage, mock_inactive):
"""Test getting filtered optimization recommendations."""
# Mock the helper functions to return test data
mock_inactive.return_value = [
{
"sys_id": "item1",
"name": "Old Laptop",
"short_description": "Outdated laptop model",
"category": "hardware",
},
]
mock_low_usage.return_value = [
{
"sys_id": "item2",
"name": "Rarely Used Software",
"short_description": "Software with few users",
"category": "software",
"order_count": 2,
},
]
# Create the parameters with only specific recommendation types
params = OptimizationRecommendationsParams(
recommendation_types=["inactive_items", "low_usage"]
)
# Call the function
result = get_optimization_recommendations(self.config, self.auth_manager, params)
# Verify the results
self.assertTrue(result["success"])
self.assertEqual(len(result["recommendations"]), 2)
# Check each recommendation type
recommendation_types = [rec["type"] for rec in result["recommendations"]]
self.assertIn("inactive_items", recommendation_types)
self.assertIn("low_usage", recommendation_types)
self.assertNotIn("high_abandonment", recommendation_types)
self.assertNotIn("slow_fulfillment", recommendation_types)
self.assertNotIn("description_quality", recommendation_types)
@patch("requests.patch")
def test_update_catalog_item(self, mock_patch):
"""Test updating a catalog item."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": {
"sys_id": "item1",
"name": "Laptop",
"short_description": "Updated laptop description",
"description": "Detailed description",
"category": "hardware",
"price": "999.99",
"active": "true",
"order": "100",
}
}
mock_patch.return_value = mock_response
# Create the parameters
params = UpdateCatalogItemParams(
item_id="item1",
short_description="Updated laptop description",
)
# Call the function
result = update_catalog_item(self.config, self.auth_manager, params)
# Verify the results
self.assertTrue(result["success"])
self.assertEqual(result["data"]["short_description"], "Updated laptop description")
# Verify the API call
mock_patch.assert_called_once()
args, kwargs = mock_patch.call_args
self.assertEqual(args[0], "https://example.service-now.com/api/now/table/sc_cat_item/item1")
self.assertEqual(kwargs["json"], {"short_description": "Updated laptop description"})
@patch("requests.patch")
def test_update_catalog_item_multiple_fields(self, mock_patch):
"""Test updating multiple fields of a catalog item."""
# Mock the response from ServiceNow
mock_response = MagicMock()
mock_response.json.return_value = {
"result": {
"sys_id": "item1",
"name": "Updated Laptop",
"short_description": "Updated laptop description",
"description": "Detailed description",
"category": "hardware",
"price": "1099.99",
"active": "true",
"order": "100",
}
}
mock_patch.return_value = mock_response
# Create the parameters with multiple fields
params = UpdateCatalogItemParams(
item_id="item1",
name="Updated Laptop",
short_description="Updated laptop description",
price="1099.99",
)
# Call the function
result = update_catalog_item(self.config, self.auth_manager, params)
# Verify the results
self.assertTrue(result["success"])
self.assertEqual(result["data"]["name"], "Updated Laptop")
self.assertEqual(result["data"]["short_description"], "Updated laptop description")
self.assertEqual(result["data"]["price"], "1099.99")
# Verify the API call
mock_patch.assert_called_once()
args, kwargs = mock_patch.call_args
self.assertEqual(args[0], "https://example.service-now.com/api/now/table/sc_cat_item/item1")
self.assertEqual(kwargs["json"], {
"name": "Updated Laptop",
"short_description": "Updated laptop description",
"price": "1099.99",
})
@patch("requests.patch")
def test_update_catalog_item_error(self, mock_patch):
"""Test error handling when updating a catalog item."""
# Mock an error response
mock_patch.side_effect = requests.exceptions.RequestException("API Error")
# Create the parameters
params = UpdateCatalogItemParams(
item_id="item1",
short_description="Updated laptop description",
)
# Call the function
result = update_catalog_item(self.config, self.auth_manager, params)
# Verify the results
self.assertFalse(result["success"])
self.assertIn("Error updating catalog item", result["message"])
self.assertIsNone(result["data"])
if __name__ == "__main__":
unittest.main()
ID: wfdzusqbvb