feat(api-hooks): Implement ApiHookClient with comprehensive tests

This commit is contained in:
2026-02-23 12:54:16 -05:00
parent 926cebe40a
commit f4a9ff82fa
2 changed files with 192 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
import pytest
import requests
from unittest.mock import MagicMock, patch
import threading
import time
import json
# Import HookServer from api_hooks.py
from api_hooks import HookServer # No need for HookServerInstance, HookHandler here
from api_hook_client import ApiHookClient
@pytest.fixture(scope="module")
def hook_server_fixture():
# Mock the 'app' object that HookServer expects
mock_app = MagicMock()
mock_app.test_hooks_enabled = True # Essential for the server to start
mock_app.project = {'name': 'test_project'}
mock_app.disc_entries = [{'role': 'user', 'content': 'hello'}]
mock_app._pending_gui_tasks = []
mock_app._pending_gui_tasks_lock = threading.Lock()
# Use an ephemeral port (0) to avoid conflicts
server = HookServer(mock_app, port=0)
server.start()
# Wait a moment for the server thread to start and bind
time.sleep(0.1)
# Get the actual port assigned by the OS
actual_port = server.server.server_address[1]
# Update the base_url for the client to use the actual port
client_base_url = f"http://127.0.0.1:{actual_port}"
yield client_base_url, mock_app # Yield the base URL and the mock_app
server.stop()
def test_get_status_success(hook_server_fixture):
"""
Test that get_status successfully retrieves the server status
when the HookServer is running. This is the 'Green Phase'.
"""
base_url, _ = hook_server_fixture
client = ApiHookClient(base_url=base_url)
status = client.get_status()
assert status == {'status': 'ok'}
def test_get_project_success(hook_server_fixture):
"""
Test successful retrieval of project data.
"""
base_url, mock_app = hook_server_fixture
client = ApiHookClient(base_url=base_url)
project = client.get_project()
assert project == {'project': mock_app.project}
def test_post_project_success(hook_server_fixture):
"""Test successful posting and updating of project data."""
base_url, mock_app = hook_server_fixture
client = ApiHookClient(base_url=base_url)
new_project_data = {'name': 'updated_project', 'version': '1.0'}
response = client.post_project(new_project_data)
assert response == {'status': 'updated'}
# Verify that the mock_app.project was updated. Note: the mock_app is reused.
# The actual server state is in the real app, but for testing client, we check mock.
# This part depends on how the actual server modifies the app.project.
# For HookHandler, it does `app.project = data.get('project', app.project)`
# So, the mock_app.project will actually be the *old* value, because the mock_app
# is not the real app instance. This test is primarily for the client-server interaction.
# To test the side effect on app.project, one would need to inspect the server's app instance,
# which is not directly exposed by the fixture in a simple way.
# For now, we focus on the client's ability to send and receive the success status.
def test_get_session_success(hook_server_fixture):
"""
Test successful retrieval of session data.
"""
base_url, mock_app = hook_server_fixture
client = ApiHookClient(base_url=base_url)
session = client.get_session()
assert session == {'session': {'entries': mock_app.disc_entries}}
def test_post_session_success(hook_server_fixture):
"""
Test successful posting and updating of session data.
"""
base_url, mock_app = hook_server_fixture
client = ApiHookClient(base_url=base_url)
new_session_entries = [{'role': 'agent', 'content': 'hi'}]
response = client.post_session(new_session_entries)
assert response == {'status': 'updated'}
# Similar note as post_project about mock_app.disc_entries not being updated here.
def test_post_gui_success(hook_server_fixture):
"""
Test successful posting of GUI data.
"""
base_url, mock_app = hook_server_fixture
client = ApiHookClient(base_url=base_url)
gui_data = {'command': 'set_text', 'id': 'some_item', 'value': 'new_text'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
assert mock_app._pending_gui_tasks == [gui_data] # This should be updated by the server logic.
def test_get_status_connection_error_handling():
"""
Test that ApiHookClient correctly handles a connection error.
"""
client = ApiHookClient(base_url="http://127.0.0.1:1") # Use a port that is highly unlikely to be listening
with pytest.raises(requests.exceptions.Timeout):
client.get_status()
def test_post_project_server_error_handling(hook_server_fixture):
"""
Test that ApiHookClient correctly handles a server-side error (e.g., 500).
This requires mocking the server\'s response within the fixture or a specific test.
For simplicity, we\'ll simulate this by causing the HookHandler to raise an exception
for a specific path, but that\'s complex with the current fixture.
A simpler way for client-side testing is to mock the requests call directly for this scenario.
"""
base_url, _ = hook_server_fixture
client = ApiHookClient(base_url=base_url)
with patch('requests.post') as mock_post:
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("500 Server Error", response=mock_response)
mock_response.text = "Internal Server Error"
mock_post.return_value = mock_response
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
client.post_project({'name': 'error_project'})
assert "HTTP error 500" in str(excinfo.value)
def test_unsupported_method_error():
"""
Test that calling an unsupported HTTP method raises a ValueError.
"""
client = ApiHookClient()
with pytest.raises(ValueError, match="Unsupported HTTP method"):
client._make_request('PUT', '/some_endpoint', data={'key': 'value'})