mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-03-01 07:41:37 +00:00
End 2 end tests (#2266)
* All endpoints covered test_integration.py kept for backwards compatability. tests/integration/run_all.py proposed as alternative to cover all endpoints. * Linter fixes
This commit is contained in:
519
tests/integration/test_tools.py
Normal file
519
tests/integration/test_tools.py
Normal file
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for DocsGPT tools management endpoints.
|
||||
|
||||
Endpoints tested:
|
||||
- /api/create_tool (POST) - Create tool
|
||||
- /api/get_tools (GET) - List tools
|
||||
- /api/update_tool (POST) - Update tool
|
||||
- /api/delete_tool (POST) - Delete tool
|
||||
- /api/update_tool_actions (POST) - Update tool actions
|
||||
- /api/update_tool_config (POST) - Update tool config
|
||||
- /api/update_tool_status (POST) - Update tool status
|
||||
- /api/available_tools (GET) - List available tools
|
||||
|
||||
Usage:
|
||||
python tests/integration/test_tools.py
|
||||
python tests/integration/test_tools.py --base-url http://localhost:7091
|
||||
python tests/integration/test_tools.py --token YOUR_JWT_TOKEN
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Add parent directory to path for standalone execution
|
||||
_THIS_DIR = Path(__file__).parent
|
||||
_TESTS_DIR = _THIS_DIR.parent
|
||||
_ROOT_DIR = _TESTS_DIR.parent
|
||||
if str(_ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT_DIR))
|
||||
|
||||
from tests.integration.base import DocsGPTTestBase, create_client_from_args
|
||||
|
||||
|
||||
class ToolsTests(DocsGPTTestBase):
|
||||
"""Integration tests for tools management endpoints."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Test Data Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def get_or_create_test_tool(self) -> Optional[str]:
|
||||
"""
|
||||
Get or create a test tool.
|
||||
|
||||
Returns:
|
||||
Tool ID or None if creation fails
|
||||
"""
|
||||
if hasattr(self, "_test_tool_id"):
|
||||
return self._test_tool_id
|
||||
|
||||
if not self.is_authenticated:
|
||||
return None
|
||||
|
||||
# CreateToolModel: 'name' must be an available tool type (e.g., "duckduckgo")
|
||||
# Use a tool that doesn't require config (like duckduckgo)
|
||||
# Note: status must be a boolean (False = draft, True = active)
|
||||
payload = {
|
||||
"name": "duckduckgo", # Must match available tool name
|
||||
"displayName": f"Test DuckDuckGo {int(time.time())}",
|
||||
"description": "Integration test tool",
|
||||
"config": {},
|
||||
"status": False, # Boolean: False = draft
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.post("/api/create_tool", json=payload, timeout=10)
|
||||
if response.status_code in [200, 201]:
|
||||
result = response.json()
|
||||
tool_id = result.get("id")
|
||||
if tool_id:
|
||||
self._test_tool_id = tool_id
|
||||
return tool_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def cleanup_test_tool(self, tool_id: str) -> None:
|
||||
"""Delete a test tool (cleanup helper)."""
|
||||
if not self.is_authenticated:
|
||||
return
|
||||
try:
|
||||
self.post("/api/delete_tool", json={"id": tool_id}, timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Create Tests
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_create_tool(self) -> bool:
|
||||
"""Test creating a tool instance from available tools."""
|
||||
test_name = "Create tool"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
# 'name' must be an available tool type (e.g., "duckduckgo", "cryptoprice")
|
||||
# Note: status must be a boolean (False = draft, True = active)
|
||||
payload = {
|
||||
"name": "cryptoprice", # A tool that needs no config
|
||||
"displayName": f"Test CryptoPrice {int(time.time())}",
|
||||
"description": "Integration test created tool",
|
||||
"config": {},
|
||||
"status": False, # Boolean: False = draft
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.post("/api/create_tool", json=payload, timeout=10)
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
self.print_error(f"Expected 200/201, got {response.status_code}")
|
||||
self.print_error(f"Response: {response.text[:200]}")
|
||||
self.record_result(test_name, False, f"Status {response.status_code}")
|
||||
return False
|
||||
|
||||
result = response.json()
|
||||
tool_id = result.get("id")
|
||||
|
||||
if not tool_id:
|
||||
self.print_error("No tool ID returned")
|
||||
self.record_result(test_name, False, "No tool ID")
|
||||
return False
|
||||
|
||||
self.print_success(f"Created tool: {tool_id}")
|
||||
self.print_info(f"Name: {payload['name']}")
|
||||
self.record_result(test_name, True, f"ID: {tool_id}")
|
||||
|
||||
# Cleanup
|
||||
self.cleanup_test_tool(tool_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
def test_create_tool_with_config(self) -> bool:
|
||||
"""Test creating a tool that requires configuration."""
|
||||
test_name = "Create tool with config"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
# Use api_tool which has flexible config requirements
|
||||
# Note: status must be a boolean (False = draft, True = active)
|
||||
payload = {
|
||||
"name": "api_tool",
|
||||
"displayName": f"Test API Tool {int(time.time())}",
|
||||
"description": "Tool with custom config",
|
||||
"config": {"base_url": "https://api.example.com"},
|
||||
"status": False, # Boolean: False = draft
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.post("/api/create_tool", json=payload, timeout=10)
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
self.print_error(f"Expected 200/201, got {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status {response.status_code}")
|
||||
return False
|
||||
|
||||
result = response.json()
|
||||
tool_id = result.get("id")
|
||||
|
||||
if not tool_id:
|
||||
self.print_error("No tool ID returned")
|
||||
self.record_result(test_name, False, "No tool ID")
|
||||
return False
|
||||
|
||||
self.print_success(f"Created tool with actions: {tool_id}")
|
||||
self.record_result(test_name, True, f"ID: {tool_id}")
|
||||
|
||||
# Cleanup
|
||||
self.cleanup_test_tool(tool_id)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Read Tests
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_get_tools(self) -> bool:
|
||||
"""Test listing all tools."""
|
||||
test_name = "List tools"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
try:
|
||||
response = self.get("/api/get_tools", timeout=10)
|
||||
|
||||
if not self.assert_status(response, 200, test_name):
|
||||
return False
|
||||
|
||||
result = response.json()
|
||||
|
||||
# Handle both list and object responses
|
||||
if isinstance(result, list):
|
||||
self.print_success(f"Retrieved {len(result)} tools")
|
||||
if result:
|
||||
self.print_info(f"First: {result[0].get('name', 'N/A')}")
|
||||
self.record_result(test_name, True, f"Count: {len(result)}")
|
||||
elif isinstance(result, dict):
|
||||
# May return object with tools array
|
||||
tools = result.get("tools", result.get("data", []))
|
||||
if isinstance(tools, list):
|
||||
self.print_success(f"Retrieved {len(tools)} tools")
|
||||
else:
|
||||
self.print_success("Retrieved tools data")
|
||||
self.record_result(test_name, True, "Tools retrieved")
|
||||
else:
|
||||
self.print_warning(f"Unexpected response type: {type(result)}")
|
||||
self.record_result(test_name, True, "Response received")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
def test_get_available_tools(self) -> bool:
|
||||
"""Test listing available tool types."""
|
||||
test_name = "List available tools"
|
||||
self.print_header(test_name)
|
||||
|
||||
try:
|
||||
response = self.get("/api/available_tools", timeout=10)
|
||||
|
||||
if not self.assert_status(response, 200, test_name):
|
||||
return False
|
||||
|
||||
result = response.json()
|
||||
|
||||
# Handle both list and object responses
|
||||
if isinstance(result, list):
|
||||
self.print_success(f"Retrieved {len(result)} available tool types")
|
||||
if result:
|
||||
first = result[0]
|
||||
name = first.get('name', first) if isinstance(first, dict) else first
|
||||
self.print_info(f"First: {name}")
|
||||
self.record_result(test_name, True, f"Count: {len(result)}")
|
||||
elif isinstance(result, dict):
|
||||
# May return object with tools array
|
||||
tools = result.get("tools", result.get("available", result.get("data", [])))
|
||||
if isinstance(tools, list):
|
||||
self.print_success(f"Retrieved {len(tools)} available tools")
|
||||
else:
|
||||
self.print_success("Retrieved available tools data")
|
||||
self.record_result(test_name, True, "Tools retrieved")
|
||||
else:
|
||||
self.print_warning(f"Unexpected response type: {type(result)}")
|
||||
self.record_result(test_name, True, "Response received")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Update Tests
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_update_tool(self) -> bool:
|
||||
"""Test updating a tool."""
|
||||
test_name = "Update tool"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
tool_id = self.get_or_create_test_tool()
|
||||
if not tool_id:
|
||||
self.print_warning("Could not create test tool")
|
||||
self.record_result(test_name, True, "Skipped (no tool)")
|
||||
return True
|
||||
|
||||
new_description = f"Updated at {int(time.time())}"
|
||||
|
||||
try:
|
||||
response = self.post(
|
||||
"/api/update_tool",
|
||||
json={"id": tool_id, "description": new_description},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
self.print_success("Tool updated successfully")
|
||||
self.record_result(test_name, True, "Tool updated")
|
||||
return True
|
||||
else:
|
||||
self.print_error(f"Update failed: {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
def test_update_tool_actions(self) -> bool:
|
||||
"""Test updating tool actions."""
|
||||
test_name = "Update tool actions"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
tool_id = self.get_or_create_test_tool()
|
||||
if not tool_id:
|
||||
self.print_warning("Could not create test tool")
|
||||
self.record_result(test_name, True, "Skipped (no tool)")
|
||||
return True
|
||||
|
||||
new_actions = [
|
||||
{
|
||||
"name": "new_action",
|
||||
"description": "New action added",
|
||||
"parameters": {},
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
response = self.post(
|
||||
"/api/update_tool_actions",
|
||||
json={"id": tool_id, "actions": new_actions},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
self.print_success("Tool actions updated")
|
||||
self.record_result(test_name, True, "Actions updated")
|
||||
return True
|
||||
else:
|
||||
self.print_error(f"Update failed: {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
def test_update_tool_config(self) -> bool:
|
||||
"""Test updating tool configuration."""
|
||||
test_name = "Update tool config"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
tool_id = self.get_or_create_test_tool()
|
||||
if not tool_id:
|
||||
self.print_warning("Could not create test tool")
|
||||
self.record_result(test_name, True, "Skipped (no tool)")
|
||||
return True
|
||||
|
||||
new_config = {"api_key": "updated_key", "timeout": 30}
|
||||
|
||||
try:
|
||||
response = self.post(
|
||||
"/api/update_tool_config",
|
||||
json={"id": tool_id, "config": new_config},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
self.print_success("Tool config updated")
|
||||
self.record_result(test_name, True, "Config updated")
|
||||
return True
|
||||
else:
|
||||
self.print_error(f"Update failed: {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
def test_update_tool_status(self) -> bool:
|
||||
"""Test updating tool status."""
|
||||
test_name = "Update tool status"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
tool_id = self.get_or_create_test_tool()
|
||||
if not tool_id:
|
||||
self.print_warning("Could not create test tool")
|
||||
self.record_result(test_name, True, "Skipped (no tool)")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Status is a boolean in UpdateToolStatusModel
|
||||
response = self.post(
|
||||
"/api/update_tool_status",
|
||||
json={"id": tool_id, "status": True},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
self.print_success("Tool status updated to active")
|
||||
self.record_result(test_name, True, "Status updated")
|
||||
return True
|
||||
else:
|
||||
self.print_error(f"Update failed: {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Delete Tests
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_delete_tool(self) -> bool:
|
||||
"""Test deleting a tool."""
|
||||
test_name = "Delete tool"
|
||||
self.print_header(test_name)
|
||||
|
||||
if not self.require_auth(test_name):
|
||||
return True
|
||||
|
||||
# Create a tool specifically for deletion - must use available tool name
|
||||
# Note: status must be a boolean (False = draft, True = active)
|
||||
payload = {
|
||||
"name": "duckduckgo",
|
||||
"displayName": f"Tool to Delete {int(time.time())}",
|
||||
"description": "Will be deleted",
|
||||
"config": {},
|
||||
"status": False, # Boolean: False = draft
|
||||
}
|
||||
|
||||
try:
|
||||
create_response = self.post("/api/create_tool", json=payload, timeout=10)
|
||||
if create_response.status_code not in [200, 201]:
|
||||
self.print_warning("Could not create tool for deletion")
|
||||
self.record_result(test_name, True, "Skipped (create failed)")
|
||||
return True
|
||||
|
||||
tool_id = create_response.json().get("id")
|
||||
|
||||
# Delete the tool (DeleteToolModel requires 'id')
|
||||
response = self.post("/api/delete_tool", json={"id": tool_id}, timeout=10)
|
||||
|
||||
if response.status_code in [200, 204]:
|
||||
self.print_success(f"Deleted tool: {tool_id}")
|
||||
self.record_result(test_name, True, "Tool deleted")
|
||||
return True
|
||||
else:
|
||||
self.print_error(f"Delete failed: {response.status_code}")
|
||||
self.record_result(test_name, False, f"Status: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error(f"Exception: {e}")
|
||||
self.record_result(test_name, False, str(e))
|
||||
return False
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Test Runner
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def run_all(self) -> bool:
|
||||
"""Run all tools tests."""
|
||||
self.print_header("DocsGPT Tools Integration Tests")
|
||||
self.print_info(f"Base URL: {self.base_url}")
|
||||
self.print_info(f"Auth: {self.token_source}")
|
||||
|
||||
# Create tests
|
||||
self.test_create_tool()
|
||||
self.test_create_tool_with_config()
|
||||
|
||||
# Read tests
|
||||
self.test_get_tools()
|
||||
self.test_get_available_tools()
|
||||
|
||||
# Update tests
|
||||
self.test_update_tool()
|
||||
self.test_update_tool_actions()
|
||||
self.test_update_tool_config()
|
||||
self.test_update_tool_status()
|
||||
|
||||
# Delete tests
|
||||
self.test_delete_tool()
|
||||
|
||||
# Cleanup
|
||||
if hasattr(self, "_test_tool_id"):
|
||||
self.cleanup_test_tool(self._test_tool_id)
|
||||
|
||||
return self.print_summary()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
client = create_client_from_args(ToolsTests, "DocsGPT Tools Integration Tests")
|
||||
exit_code = 0 if client.run_all() else 1
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user