Files
core-api/tests/test_tools_controller.py
T
jpmschweitzerandClaude 0c7e1cb04e fix(tests): mock the live DNS service instance, not a dead legacy class (T-55)
test_dns_lookup_returns_result patched src.controllers.tools_controller.
DNSService, which is never imported by the request path under test.
client wraps src.main.app, which routes /tools/dns/lookup through
src.domains.tools.controller.tools_controller — a singleton constructed
at import time from src.domains.tools.dns.service.DNSService. The patched
class was dead; the mock was never consulted, so the test issued a real
DNS query for example.com and asserted on its outcome. With no network
the query times out and the assertion fails (D-26).

The three sibling tests in the same class patch the same dead class and
also run unmocked, but happen not to notice: DNS failures are caught
inside DNSService.lookup() and returned as a normal 200 response with
success=False, and their assertions only check status_code / DNSQueryError
branches that don't depend on resolution actually succeeding. Only this
test's `data["success"] is True` assertion is sensitive to the real
network outcome, which is why it's the only one D-26's namespace run
catches. Not touched here — out of T-55's scope, flagging for the record.

Fix patches tools_controller.dns_service, the actual instance attribute
the live route calls, via patch.object on the singleton rather than
patch() on the constructor class (the instance already exists by the
time a class-level patch would apply).

Verified:
- unshare -rn (lo up): 381 passed, exit 0 (was 1 failed, 380 passed, exit 2)
- with network: 381 passed, exit 0 (unchanged from before the fix)
- mutation check: retargeted the patch.object to a nonexistent attribute
  name, confirmed count==1 before editing; namespace run then reproduced
  the original failure (1 failed, 380 passed); reverted and reconfirmed
  381 passed, exit 0.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 15:33:58 +02:00

156 lines
5.6 KiB
Python

"""Tests for tools controller."""
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock, MagicMock
from src.main import app
from src.domains.tools.controller import tools_controller
@pytest.fixture
def client():
"""Create a test client."""
return TestClient(app)
class TestDNSLookup:
"""Test /tools/dns/lookup endpoint."""
@patch("src.controllers.tools_controller.DNSService")
def test_dns_lookup_returns_200(self, mock_dns_class, client):
"""DNS lookup should return 200 for valid request."""
mock_service = MagicMock()
mock_service.lookup = AsyncMock(return_value=MagicMock(
success=True,
domain="example.com",
record_type="A",
records=[{"value": "93.184.216.34"}],
nameserver_used="8.8.8.8",
query_time_ms=50,
error_message=None
))
mock_dns_class.return_value = mock_service
response = client.post(
"/tools/dns/lookup",
json={"domain": "example.com", "record_type": "A"}
)
assert response.status_code == 200
def test_dns_lookup_returns_result(self, client):
"""DNS lookup should return lookup results.
Patches the live singleton's `dns_service` attribute, not the
`src.controllers.tools_controller.DNSService` class: that module is
the legacy top-level package (not wired into `src.main`, see
CLAUDE.md "Legacy top-level packages"). `client` exercises
`src.main.app`, which routes through
`src.domains.tools.controller.tools_controller`, a singleton built
at import time — so patching the class there would also miss,
since `tools_controller.dns_service` is already a constructed
instance by the time a test patches the class. Patching the
instance attribute directly is the only patch that actually
intercepts this request path.
"""
mock_response = MagicMock()
mock_response.success = True
mock_response.domain = "example.com"
mock_response.record_type = "A"
mock_response.records = [{"value": "93.184.216.34"}]
mock_response.nameserver_used = "8.8.8.8"
mock_response.query_time_ms = 50
mock_response.error_message = None
mock_response.model_dump = MagicMock(return_value={
"success": True,
"domain": "example.com",
"record_type": "A",
"records": [{"value": "93.184.216.34"}],
"nameserver_used": "8.8.8.8",
"query_time_ms": 50,
"error_message": None
})
mock_service = MagicMock()
mock_service.lookup = AsyncMock(return_value=mock_response)
with patch.object(tools_controller, "dns_service", mock_service):
response = client.post(
"/tools/dns/lookup",
json={"domain": "example.com", "record_type": "A"}
)
data = response.json()
assert data["success"] is True
assert data["domain"] == "example.com"
def test_dns_lookup_requires_domain(self, client):
"""DNS lookup should require domain parameter."""
response = client.post(
"/tools/dns/lookup",
json={"record_type": "A"}
)
assert response.status_code == 422
@patch("src.controllers.tools_controller.DNSService")
def test_dns_lookup_handles_dns_query_error(self, mock_dns_class, client):
"""DNS lookup should handle DNSQueryError."""
from src.dns.exceptions import DNSQueryError
mock_service = MagicMock()
mock_service.lookup = AsyncMock(side_effect=DNSQueryError("Unsupported record type"))
mock_dns_class.return_value = mock_service
response = client.post(
"/tools/dns/lookup",
json={"domain": "example.com", "record_type": "INVALID"}
)
assert response.status_code == 400
@patch("src.controllers.tools_controller.DNSService")
def test_dns_lookup_accepts_custom_nameserver(self, mock_dns_class, client):
"""DNS lookup should accept custom nameserver."""
mock_response = MagicMock()
mock_response.success = True
mock_response.domain = "example.com"
mock_response.record_type = "A"
mock_response.records = []
mock_response.nameserver_used = "1.1.1.1"
mock_response.query_time_ms = 30
mock_response.error_message = None
mock_response.model_dump = MagicMock(return_value={
"success": True,
"domain": "example.com",
"record_type": "A",
"records": [],
"nameserver_used": "1.1.1.1",
"query_time_ms": 30,
"error_message": None
})
mock_service = MagicMock()
mock_service.lookup = AsyncMock(return_value=mock_response)
mock_dns_class.return_value = mock_service
response = client.post(
"/tools/dns/lookup",
json={"domain": "example.com", "record_type": "A", "nameserver": "1.1.1.1"}
)
assert response.status_code == 200
class TestToolsControllerInit:
"""Test ToolsController initialization."""
def test_controller_has_correct_prefix(self):
"""Controller should have /tools prefix."""
from src.controllers.tools_controller import tools_controller
assert tools_controller.prefix == "/tools"
def test_controller_has_correct_tags(self):
"""Controller should have Tools tag."""
from src.controllers.tools_controller import tools_controller
assert "Tools" in tools_controller.tags