Do full rewrite of 5xx => 4xx

This commit is contained in:
Jack Kingsman
2026-04-30 18:47:35 -07:00
parent f710a1f2d9
commit dbf14259dc
17 changed files with 114 additions and 114 deletions

View File

@@ -50,7 +50,7 @@ def _patch_require_connected(mc=None, *, detail="Radio not connected"):
if mc is None:
return patch(
"app.services.radio_runtime.radio_runtime.require_connected",
side_effect=HTTPException(status_code=503, detail=detail),
side_effect=HTTPException(status_code=423, detail=detail),
)
return patch("app.services.radio_runtime.radio_runtime.require_connected", return_value=mc)
@@ -422,11 +422,11 @@ class TestDebugEndpoint:
class TestRadioDisconnectedHandler:
"""Test that RadioDisconnectedError maps to 503."""
"""Test that RadioDisconnectedError maps to 423."""
@pytest.mark.asyncio
async def test_disconnect_race_returns_503(self, test_db, client):
"""If radio disconnects between require_connected() and lock acquisition, return 503."""
async def test_disconnect_race_returns_423(self, test_db, client):
"""If radio disconnects between require_connected() and lock acquisition, return 423."""
pub_key = "ab" * 32
await _insert_contact(pub_key, "Alice")
@@ -437,7 +437,7 @@ class TestRadioDisconnectedHandler:
"/api/messages/direct", json={"destination": pub_key, "text": "Hi"}
)
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@@ -500,25 +500,25 @@ class TestMessagesEndpoint:
@pytest.mark.asyncio
async def test_send_direct_message_requires_connection(self, test_db, client):
"""Sending message when disconnected returns 503."""
"""Sending message when disconnected returns 423."""
with _patch_require_connected():
response = await client.post(
"/api/messages/direct", json={"destination": "abc123", "text": "Hello"}
)
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_send_channel_message_requires_connection(self, test_db, client):
"""Sending channel message when disconnected returns 503."""
"""Sending channel message when disconnected returns 423."""
with _patch_require_connected():
response = await client.post(
"/api/messages/channel",
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"},
)
assert response.status_code == 503
assert response.status_code == 423
@pytest.mark.asyncio
async def test_send_direct_message_emits_websocket_message_event(self, test_db, client):
@@ -603,8 +603,8 @@ class TestMessagesEndpoint:
assert "not found" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_send_direct_message_duplicate_returns_500(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 500."""
async def test_send_direct_message_duplicate_returns_422(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 422."""
from app.models import SendDirectMessageRequest
from app.routers.messages import send_direct_message
@@ -636,12 +636,12 @@ class TestMessagesEndpoint:
SendDirectMessageRequest(destination=pub_key, text="Hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "unexpected duplicate" in exc_info.value.detail.lower()
@pytest.mark.asyncio
async def test_send_channel_message_duplicate_returns_500(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 500."""
async def test_send_channel_message_duplicate_returns_422(self, test_db):
"""If MessageRepository.create returns None (duplicate), returns 422."""
from app.models import SendChannelMessageRequest
from app.routers.messages import send_channel_message
@@ -672,16 +672,16 @@ class TestMessagesEndpoint:
SendChannelMessageRequest(channel_key=chan_key, text="Hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "unexpected duplicate" in exc_info.value.detail.lower()
@pytest.mark.asyncio
async def test_resend_channel_message_requires_connection(self, test_db, client):
"""Resend endpoint returns 503 when radio is disconnected."""
"""Resend endpoint returns 423 when radio is disconnected."""
with _patch_require_connected():
response = await client.post("/api/messages/channel/1/resend")
assert response.status_code == 503
assert response.status_code == 423
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio

View File

@@ -709,7 +709,7 @@ class TestBotMessageRateLimiting:
patch(
"app.routers.messages.send_direct_message",
new_callable=AsyncMock,
side_effect=HTTPException(status_code=500, detail="Send failed"),
side_effect=HTTPException(status_code=422, detail="Send failed"),
),
):
await process_bot_response(

View File

@@ -317,7 +317,7 @@ class TestPathDiscovery:
mock_broadcast.assert_called_once_with("contact", updated.model_dump())
@pytest.mark.asyncio
async def test_returns_504_when_no_response_is_heard(self, test_db, client):
async def test_returns_408_when_no_response_is_heard(self, test_db, client):
await _insert_contact(KEY_A, "Alice", type=1)
mc = MagicMock()
mc.commands = MagicMock()
@@ -332,7 +332,7 @@ class TestPathDiscovery:
mock_rm.radio_operation = _noop_radio_operation(mc)
response = await client.post(f"/api/contacts/{KEY_A}/path-discovery")
assert response.status_code == 504
assert response.status_code == 408
assert "No path discovery response heard" in response.json()["detail"]

View File

@@ -174,8 +174,8 @@ class TestRadioOperationYield:
class TestRequireConnected:
"""Test the require_connected() FastAPI dependency."""
def test_raises_503_when_setup_in_progress(self):
"""HTTPException 503 is raised when radio is connected but setup is still in progress."""
def test_raises_423_when_setup_in_progress(self):
"""HTTPException 423 is raised when radio is connected but setup is still in progress."""
from fastapi import HTTPException
from app.services.radio_runtime import radio_runtime
@@ -188,11 +188,11 @@ class TestRequireConnected:
with pytest.raises(HTTPException) as exc_info:
radio_runtime.require_connected()
assert exc_info.value.status_code == 503
assert exc_info.value.status_code == 423
assert "initializing" in exc_info.value.detail.lower()
def test_raises_503_when_not_connected(self):
"""HTTPException 503 is raised when radio is not connected."""
def test_raises_423_when_not_connected(self):
"""HTTPException 423 is raised when radio is not connected."""
from fastapi import HTTPException
from app.services.radio_runtime import radio_runtime
@@ -205,7 +205,7 @@ class TestRequireConnected:
with pytest.raises(HTTPException) as exc_info:
radio_runtime.require_connected()
assert exc_info.value.status_code == 503
assert exc_info.value.status_code == 423
def test_returns_meshcore_when_connected_and_setup_complete(self):
"""Returns meshcore instance when radio is connected and setup is complete."""

View File

@@ -131,14 +131,14 @@ class TestGetRadioConfig:
assert response.advert_location_source == "current"
@pytest.mark.asyncio
async def test_returns_503_when_self_info_missing(self):
async def test_returns_423_when_self_info_missing(self):
mc = MagicMock()
mc.self_info = None
with patch("app.routers.radio.radio_manager.require_connected", return_value=mc):
with pytest.raises(HTTPException) as exc:
await get_radio_config()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
class TestUpdateRadioConfig:
@@ -278,7 +278,7 @@ class TestUpdateRadioConfig:
with pytest.raises(HTTPException) as exc:
await update_radio_config(RadioConfigUpdate(path_hash_mode=1))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to set path hash mode" in str(exc.value.detail)
assert radio_manager.path_hash_mode == 0
mc.commands.send_appstart.assert_not_awaited()
@@ -339,7 +339,7 @@ class TestPrivateKeyImport:
with pytest.raises(HTTPException) as exc:
await set_private_key(PrivateKeyUpdate(private_key="aa" * 64))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
class TestDiscoverMesh:
@@ -699,7 +699,7 @@ class TestTracePath:
assert "not a repeater" in exc.value.detail
@pytest.mark.asyncio
async def test_returns_422_when_no_trace_response_is_heard(self):
async def test_returns_408_when_no_trace_response_is_heard(self):
mc = _mock_meshcore_with_info()
repeater = Contact(
public_key="44" * 32,
@@ -741,7 +741,7 @@ class TestTracePath:
)
)
assert exc.value.status_code == 422
assert exc.value.status_code == 408
assert "No trace response heard" in exc.value.detail
@pytest.mark.asyncio
@@ -850,7 +850,7 @@ class TestTracePath:
with pytest.raises(HTTPException) as exc:
await discover_mesh(RadioDiscoveryRequest(target="sensors"))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert exc.value.detail == "Failed to start mesh discovery"
@pytest.mark.asyncio
@@ -887,7 +887,7 @@ class TestTracePath:
with pytest.raises(HTTPException) as exc:
await set_private_key(PrivateKeyUpdate(private_key="aa" * 64))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "keystore" in exc.value.detail.lower()
# Called twice: initial attempt + one retry
assert mock_export.await_count == 2
@@ -926,7 +926,7 @@ class TestAdvertise:
with pytest.raises(HTTPException) as exc:
await send_advertisement()
assert exc.value.status_code == 500
assert exc.value.status_code == 422
@pytest.mark.asyncio
async def test_defaults_to_flood_mode(self):
@@ -1059,7 +1059,7 @@ class TestRebootAndReconnect:
assert result["connected"] is True
@pytest.mark.asyncio
async def test_reconnect_raises_503_on_failure(self):
async def test_reconnect_raises_423_on_failure(self):
mock_rm = MagicMock()
mock_rm.is_connected = False
mock_rm.is_reconnecting = False
@@ -1070,7 +1070,7 @@ class TestRebootAndReconnect:
with pytest.raises(HTTPException) as exc:
await reconnect_radio()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
@pytest.mark.asyncio
async def test_disconnect_pauses_connection_attempts_and_broadcasts_health(self):

View File

@@ -57,12 +57,12 @@ def test_require_connected_preserves_http_semantics():
)
with pytest.raises(HTTPException, match="Radio is initializing") as exc:
runtime.require_connected()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
runtime = RadioRuntime(_Manager(meshcore=None, is_connected=False, is_setup_in_progress=False))
with pytest.raises(HTTPException, match="Radio not connected") as exc:
runtime.require_connected()
assert exc.value.status_code == 503
assert exc.value.status_code == 423
def test_require_connected_returns_fresh_meshcore_after_connectivity_check():

View File

@@ -302,7 +302,7 @@ class TestRepeaterCommandRoute:
with pytest.raises(HTTPException) as exc:
await send_repeater_command(KEY_A, CommandRequest(command="ver"))
assert exc.value.status_code == 500
assert exc.value.status_code == 422
mc.start_auto_message_fetching.assert_awaited_once()
@pytest.mark.asyncio
@@ -502,7 +502,7 @@ class TestTraceRoute:
with pytest.raises(HTTPException) as exc:
await request_trace(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
mc.commands.send_trace.assert_awaited_once_with(
path=KEY_A[:8],
tag=1234,
@@ -510,7 +510,7 @@ class TestTraceRoute:
)
@pytest.mark.asyncio
async def test_wait_timeout_returns_422(self, test_db):
async def test_wait_timeout_returns_408(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Client", contact_type=1)
mc.commands.send_trace = AsyncMock(return_value=_radio_result(EventType.OK))
@@ -524,7 +524,7 @@ class TestTraceRoute:
with pytest.raises(HTTPException) as exc:
await request_trace(KEY_A)
assert exc.value.status_code == 422
assert exc.value.status_code == 408
mc.commands.send_trace.assert_awaited_once_with(
path=KEY_A[:8],
tag=1234,
@@ -745,7 +745,7 @@ class TestRepeaterStatus:
assert response.recv_errors == 42
@pytest.mark.asyncio
async def test_504_on_timeout(self, test_db):
async def test_408_on_timeout(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
mc.commands.req_status_sync = AsyncMock(return_value=None)
@@ -756,7 +756,7 @@ class TestRepeaterStatus:
):
with pytest.raises(HTTPException) as exc:
await repeater_status(KEY_A)
assert exc.value.status_code == 504
assert exc.value.status_code == 408
@pytest.mark.asyncio
async def test_400_not_repeater(self, test_db):
@@ -819,7 +819,7 @@ class TestRepeaterLppTelemetry:
assert response.sensors == []
@pytest.mark.asyncio
async def test_504_on_timeout(self, test_db):
async def test_408_on_timeout(self, test_db):
mc = _mock_mc()
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
mc.commands.req_telemetry_sync = AsyncMock(return_value=None)
@@ -830,7 +830,7 @@ class TestRepeaterLppTelemetry:
):
with pytest.raises(HTTPException) as exc:
await repeater_lpp_telemetry(KEY_A)
assert exc.value.status_code == 504
assert exc.value.status_code == 408
@pytest.mark.asyncio
async def test_400_not_repeater(self, test_db):
@@ -1234,7 +1234,7 @@ class TestBatchCliFetch:
with pytest.raises(HTTPException) as exc:
await _batch_cli_fetch(contact, "test_op", [("ver", "firmware_version")])
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1307,7 +1307,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_status(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1325,7 +1325,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_lpp_telemetry(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1343,7 +1343,7 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_neighbors(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail
@pytest.mark.asyncio
@@ -1361,5 +1361,5 @@ class TestRepeaterAddContactError:
with pytest.raises(HTTPException) as exc:
await repeater_acl(KEY_A)
assert exc.value.status_code == 500
assert exc.value.status_code == 422
assert "Failed to add contact to radio" in exc.value.detail

View File

@@ -646,7 +646,7 @@ class TestOutgoingChannelBroadcast:
request = SendChannelMessageRequest(channel_key=chan_key, text="hello")
await send_channel_message(request)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "regional override" in exc_info.value.detail.lower()
mc.commands.set_channel.assert_not_awaited()
mc.commands.send_chan_msg.assert_not_awaited()
@@ -790,7 +790,7 @@ class TestOutgoingChannelBroadcast:
SendChannelMessageRequest(channel_key=chan_key, text="this will fail")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert radio_manager.get_cached_channel_slot(chan_key) is None
@@ -969,7 +969,7 @@ class TestResendChannelMessage:
assert sent_timestamp == now + 1
@pytest.mark.asyncio
async def test_resend_no_radio_response_returns_504_and_creates_no_new_row(self, test_db):
async def test_resend_no_radio_response_returns_408_and_creates_no_new_row(self, test_db):
"""When resend returns None, report unknown outcome and create no new message row."""
mc = _make_mc(name="MyNode")
chan_key = "c1" * 16
@@ -995,7 +995,7 @@ class TestResendChannelMessage:
):
await resend_channel_message(msg_id, new_timestamp=True)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1317,7 +1317,7 @@ class TestPathHashModeOverride:
SendChannelMessageRequest(channel_key=chan_key, text="hello")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert "path hash mode" in exc_info.value.detail.lower()
mc.commands.send_chan_msg.assert_not_awaited()
@@ -1567,7 +1567,7 @@ class TestRadioExceptionMidSend:
assert len(messages) == 0
@pytest.mark.asyncio
async def test_dm_send_no_radio_response_returns_504_without_storing_message(self, test_db):
async def test_dm_send_no_radio_response_returns_408_without_storing_message(self, test_db):
"""When mc.commands.send_msg() returns None, report unknown outcome and store nothing."""
mc = _make_mc()
pub_key = "ac" * 32
@@ -1584,7 +1584,7 @@ class TestRadioExceptionMidSend:
SendDirectMessageRequest(destination=pub_key, text="Did this send?")
)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1593,7 +1593,7 @@ class TestRadioExceptionMidSend:
assert len(messages) == 0
@pytest.mark.asyncio
async def test_channel_send_no_radio_response_returns_504_without_storing_message(
async def test_channel_send_no_radio_response_returns_408_without_storing_message(
self, test_db
):
"""When mc.commands.send_chan_msg() returns None, report unknown outcome and store nothing."""
@@ -1612,7 +1612,7 @@ class TestRadioExceptionMidSend:
SendChannelMessageRequest(channel_key=chan_key, text="Did this send?")
)
assert exc_info.value.status_code == 504
assert exc_info.value.status_code == 408
assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL
messages = await MessageRepository.get_all(
@@ -1733,7 +1733,7 @@ class TestRadioExceptionMidSend:
SendChannelMessageRequest(channel_key=chan_key_b, text="Never sent")
)
assert exc_info.value.status_code == 500
assert exc_info.value.status_code == 422
assert radio_manager.get_cached_channel_slot(chan_key_a) is None
assert radio_manager.get_cached_channel_slot(chan_key_b) is None
mc.commands.send_chan_msg.assert_not_called()