Skip to content

Commit 038fb58

Browse files
Add parametrised coverage for protocol error-branch dispatch
DqliteProtocol's dispatcher methods (handshake, get_leader, open_database, prepare, finalize, exec_sql, query_sql) guard against two broken-server cases: a FailureResponse (wrapped as OperationalError with code/message preserved) and a wrong message type (wrapped as ProtocolError citing the actual type). Existing coverage only exercised finalize's wrong-type path and open_database's FailureResponse path. Add a systematic mock-based test class per method so a refactor that flips the exception taxonomy on any branch breaks the suite immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a144ead commit 038fb58

File tree

1 file changed

+141
-0
lines changed

1 file changed

+141
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Systematic coverage of wrong-response-type / FailureResponse branches.
2+
3+
DqliteProtocol's dispatcher methods each guard against two broken-
4+
server cases:
5+
6+
* the server replies with a ``FailureResponse`` instead of the
7+
opcode-specific response (mapped to ``OperationalError`` with the
8+
failure code and message preserved); and
9+
* the server replies with some other unrelated message type (mapped
10+
to ``ProtocolError`` citing the actual type received).
11+
12+
Existing coverage only exercises ``finalize``'s wrong-type path and
13+
``open_database``'s FailureResponse path; the rest is a systematic
14+
test gap. Fill it in so a refactor that accidentally flips the
15+
exception type on any branch breaks the suite immediately.
16+
"""
17+
18+
from unittest.mock import AsyncMock, MagicMock
19+
20+
import pytest
21+
22+
from dqliteclient.exceptions import OperationalError, ProtocolError
23+
from dqliteclient.protocol import DqliteProtocol
24+
from dqlitewire.messages import (
25+
DbResponse,
26+
FailureResponse,
27+
LeaderResponse,
28+
)
29+
30+
31+
@pytest.fixture
32+
def protocol() -> DqliteProtocol:
33+
reader = AsyncMock()
34+
writer = MagicMock()
35+
writer.drain = AsyncMock()
36+
writer.close = MagicMock()
37+
writer.wait_closed = AsyncMock()
38+
return DqliteProtocol(reader, writer)
39+
40+
41+
class TestHandshakeWrongResponseType:
42+
async def test_handshake_rejects_leader_response_instead_of_welcome(
43+
self, protocol: DqliteProtocol
44+
) -> None:
45+
# Feed a LeaderResponse where a Welcome is expected.
46+
protocol._reader.read.return_value = LeaderResponse( # type: ignore[attr-defined]
47+
node_id=1, address="a:1"
48+
).encode()
49+
with pytest.raises(ProtocolError, match="Expected WelcomeResponse"):
50+
await protocol.handshake()
51+
52+
53+
class TestGetLeaderErrorBranches:
54+
async def test_get_leader_failure_response(self, protocol: DqliteProtocol) -> None:
55+
protocol._reader.read.return_value = FailureResponse( # type: ignore[attr-defined]
56+
code=1, message="probe failed"
57+
).encode()
58+
# handshake must succeed first — simulate by setting the flag.
59+
protocol._handshake_done = True
60+
with pytest.raises(OperationalError) as exc_info:
61+
await protocol.get_leader()
62+
assert exc_info.value.code == 1
63+
assert "probe failed" in exc_info.value.message
64+
65+
async def test_get_leader_wrong_type(self, protocol: DqliteProtocol) -> None:
66+
protocol._reader.read.return_value = DbResponse(db_id=1).encode() # type: ignore[attr-defined]
67+
protocol._handshake_done = True
68+
with pytest.raises(ProtocolError, match="Expected LeaderResponse"):
69+
await protocol.get_leader()
70+
71+
72+
class TestOpenDatabaseWrongType:
73+
async def test_open_database_wrong_type(self, protocol: DqliteProtocol) -> None:
74+
protocol._reader.read.return_value = LeaderResponse( # type: ignore[attr-defined]
75+
node_id=1, address="a:1"
76+
).encode()
77+
protocol._handshake_done = True
78+
with pytest.raises(ProtocolError, match="Expected DbResponse"):
79+
await protocol.open_database("test.db")
80+
81+
82+
class TestPrepareErrorBranches:
83+
async def test_prepare_failure_response(self, protocol: DqliteProtocol) -> None:
84+
protocol._reader.read.return_value = FailureResponse( # type: ignore[attr-defined]
85+
code=1, message="SQL syntax error"
86+
).encode()
87+
protocol._handshake_done = True
88+
with pytest.raises(OperationalError) as exc_info:
89+
await protocol.prepare(1, "BAD SQL")
90+
assert exc_info.value.code == 1
91+
92+
async def test_prepare_wrong_type(self, protocol: DqliteProtocol) -> None:
93+
protocol._reader.read.return_value = DbResponse(db_id=1).encode() # type: ignore[attr-defined]
94+
protocol._handshake_done = True
95+
with pytest.raises(ProtocolError, match="Expected StmtResponse"):
96+
await protocol.prepare(1, "SELECT 1")
97+
98+
99+
class TestFinalizeFailureBranch:
100+
async def test_finalize_failure_response(self, protocol: DqliteProtocol) -> None:
101+
protocol._reader.read.return_value = FailureResponse( # type: ignore[attr-defined]
102+
code=21, message="misuse"
103+
).encode()
104+
protocol._handshake_done = True
105+
with pytest.raises(OperationalError) as exc_info:
106+
await protocol.finalize(1, 1)
107+
assert exc_info.value.code == 21
108+
109+
110+
class TestExecSqlErrorBranches:
111+
async def test_exec_sql_failure_response(self, protocol: DqliteProtocol) -> None:
112+
protocol._reader.read.return_value = FailureResponse( # type: ignore[attr-defined]
113+
code=19, message="constraint failed"
114+
).encode()
115+
protocol._handshake_done = True
116+
with pytest.raises(OperationalError) as exc_info:
117+
await protocol.exec_sql(1, "INSERT INTO t VALUES (1)")
118+
assert exc_info.value.code == 19
119+
120+
async def test_exec_sql_wrong_type(self, protocol: DqliteProtocol) -> None:
121+
protocol._reader.read.return_value = DbResponse(db_id=1).encode() # type: ignore[attr-defined]
122+
protocol._handshake_done = True
123+
with pytest.raises(ProtocolError, match="Expected ResultResponse"):
124+
await protocol.exec_sql(1, "INSERT INTO t VALUES (1)")
125+
126+
127+
class TestSendQueryErrorBranches:
128+
async def test_query_sql_failure_response(self, protocol: DqliteProtocol) -> None:
129+
protocol._reader.read.return_value = FailureResponse( # type: ignore[attr-defined]
130+
code=5, message="busy"
131+
).encode()
132+
protocol._handshake_done = True
133+
with pytest.raises(OperationalError) as exc_info:
134+
await protocol.query_sql(1, "SELECT 1")
135+
assert exc_info.value.code == 5
136+
137+
async def test_query_sql_wrong_type(self, protocol: DqliteProtocol) -> None:
138+
protocol._reader.read.return_value = DbResponse(db_id=1).encode() # type: ignore[attr-defined]
139+
protocol._handshake_done = True
140+
with pytest.raises(ProtocolError, match="Expected RowsResponse"):
141+
await protocol.query_sql(1, "SELECT 1")

0 commit comments

Comments
 (0)