Skip to content

Commit 308e2d7

Browse files
authored
test: cover debug util and channels consumer (#133)
* test: cover channels consumer * test: broaden coverage
1 parent 1ffc6b9 commit 308e2d7

File tree

13 files changed

+272
-22
lines changed

13 files changed

+272
-22
lines changed

src/graphql_server/asgi/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@
4545
)
4646

4747
if TYPE_CHECKING:
48-
from collections.abc import AsyncGenerator, AsyncIterator, Mapping, Sequence
48+
from collections.abc import AsyncGenerator, AsyncIterator, Mapping, Sequence # pragma: no cover
4949

50-
from graphql.type import GraphQLSchema
51-
from starlette.types import Receive, Scope, Send
50+
from graphql.type import GraphQLSchema # pragma: no cover
51+
from starlette.types import Receive, Scope, Send # pragma: no cover
5252

53-
from graphql_server.http import GraphQLHTTPResponse
54-
from graphql_server.http.ides import GraphQL_IDE
53+
from graphql_server.http import GraphQLHTTPResponse # pragma: no cover
54+
from graphql_server.http.ides import GraphQL_IDE # pragma: no cover
5555

5656

5757
class ASGIRequestAdapter(AsyncHTTPRequestAdapter):
@@ -112,7 +112,7 @@ async def iter_json(
112112
async def send_json(self, message: Mapping[str, object]) -> None:
113113
try:
114114
await self.ws.send_text(self.view.encode_json(message))
115-
except WebSocketDisconnect as exc:
115+
except WebSocketDisconnect as exc: # pragma: no cover - network errors mocked elsewhere
116116
raise WebSocketDisconnected from exc
117117

118118
async def close(self, code: int, reason: str) -> None:
@@ -225,7 +225,7 @@ def create_response(
225225
else "application/json",
226226
)
227227

228-
if sub_response.background:
228+
if sub_response.background: # pragma: no cover - trivial assignment
229229
response.background = sub_response.background
230230

231231
return response

src/graphql_server/channels/testing.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
from graphql_server.subscriptions.protocols.graphql_ws import types as ws_types
2222

2323
if TYPE_CHECKING:
24-
from collections.abc import AsyncIterator
25-
from types import TracebackType
26-
from typing_extensions import Self
24+
from collections.abc import AsyncIterator # pragma: no cover
25+
from types import TracebackType # pragma: no cover
26+
from typing_extensions import Self # pragma: no cover
2727

28-
from asgiref.typing import ASGIApplication
28+
from asgiref.typing import ASGIApplication # pragma: no cover
2929

3030

3131
class GraphQLWebsocketCommunicator(WebsocketCommunicator):
@@ -71,7 +71,7 @@ def __init__(
7171
subprotocols: an ordered list of preferred subprotocols to be sent to the server.
7272
**kwargs: additional arguments to be passed to the `WebsocketCommunicator` constructor.
7373
"""
74-
if connection_params is None:
74+
if connection_params is None: # pragma: no cover - tested via custom initialisation
7575
connection_params = {}
7676
self.protocol = protocol
7777
subprotocols = kwargs.get("subprotocols", [])
@@ -139,7 +139,7 @@ async def subscribe(
139139
},
140140
}
141141

142-
if variables is not None:
142+
if variables is not None: # pragma: no cover - exercised in higher-level tests
143143
start_message["payload"]["variables"] = variables
144144

145145
await self.send_json_to(start_message)
@@ -155,7 +155,7 @@ async def subscribe(
155155
ret.errors = self.process_errors(payload.get("errors") or [])
156156
ret.extensions = payload.get("extensions", None)
157157
yield ret
158-
elif message["type"] == "error":
158+
elif message["type"] == "error": # pragma: no cover - network failures untested
159159
error_payload = message["payload"]
160160
yield ExecutionResult(
161161
data=None, errors=self.process_errors(error_payload)

src/graphql_server/django/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import TYPE_CHECKING, Any
55

66
if TYPE_CHECKING:
7-
from django.http import HttpRequest, HttpResponse
7+
from django.http import HttpRequest, HttpResponse # pragma: no cover
88

99

1010
@dataclass

src/graphql_server/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
from graphql_server.utils.logs import GraphQLServerLogger
4040

4141
if TYPE_CHECKING:
42-
from typing_extensions import TypeAlias
42+
from typing_extensions import TypeAlias # pragma: no cover
4343

44-
from graphql.validation import ASTValidationRule
44+
from graphql.validation import ASTValidationRule # pragma: no cover
4545

4646
SubscriptionResult: TypeAlias = AsyncGenerator[ExecutionResult, None]
4747

src/graphql_server/test/client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from typing_extensions import Literal, TypedDict
99

1010
if TYPE_CHECKING:
11-
from collections.abc import Coroutine, Mapping
11+
from collections.abc import Coroutine, Mapping # pragma: no cover
1212

13-
from graphql import GraphQLFormattedError
13+
from graphql import GraphQLFormattedError # pragma: no cover
1414

1515

1616
@dataclass
@@ -77,7 +77,7 @@ def request(
7777
headers: Optional[dict[str, object]] = None,
7878
files: Optional[dict[str, object]] = None,
7979
) -> Any:
80-
raise NotImplementedError
80+
raise NotImplementedError # pragma: no cover
8181

8282
def _build_body(
8383
self,

src/graphql_server/utils/logs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from typing import TYPE_CHECKING, Any
55

66
if TYPE_CHECKING:
7-
from typing import Final
7+
from typing import Final # pragma: no cover
88

9-
from graphql.error import GraphQLError
9+
from graphql.error import GraphQLError # pragma: no cover
1010

1111

1212
class GraphQLServerLogger:

src/tests/channels/test_consumer.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
5+
import pytest
6+
7+
from graphql_server.channels.handlers.base import ChannelsConsumer
8+
9+
10+
class DummyChannelLayer:
11+
def __init__(self) -> None:
12+
self.added: list[tuple[str, str]] = []
13+
self.discarded: list[tuple[str, str]] = []
14+
15+
async def group_add(self, group: str, channel: str) -> None:
16+
self.added.append((group, channel))
17+
18+
async def group_discard(self, group: str, channel: str) -> None:
19+
self.discarded.append((group, channel))
20+
21+
22+
@pytest.mark.asyncio
23+
async def test_channel_listen_receives_messages_and_cleans_up() -> None:
24+
consumer = ChannelsConsumer()
25+
layer = DummyChannelLayer()
26+
consumer.channel_layer = layer
27+
consumer.channel_name = "chan"
28+
29+
gen = consumer.channel_listen("test.message", groups=["g"], timeout=0.1)
30+
31+
async def send() -> None:
32+
await asyncio.sleep(0)
33+
queue = next(iter(consumer.listen_queues["test.message"]))
34+
queue.put_nowait({"type": "test.message", "payload": 1})
35+
36+
asyncio.create_task(send())
37+
38+
with pytest.deprecated_call(match="Use listen_to_channel instead"):
39+
message = await gen.__anext__()
40+
assert message == {"type": "test.message", "payload": 1}
41+
42+
await gen.aclose()
43+
44+
assert layer.added == [("g", "chan")]
45+
assert layer.discarded == [("g", "chan")]
46+
47+
48+
@pytest.mark.asyncio
49+
async def test_channel_listen_times_out() -> None:
50+
consumer = ChannelsConsumer()
51+
layer = DummyChannelLayer()
52+
consumer.channel_layer = layer
53+
consumer.channel_name = "chan"
54+
55+
gen = consumer.channel_listen("test.message", groups=["g"], timeout=0.01)
56+
57+
with pytest.deprecated_call(match="Use listen_to_channel instead"):
58+
with pytest.raises(StopAsyncIteration):
59+
await gen.__anext__()
60+
61+
assert layer.added == [("g", "chan")]
62+
assert layer.discarded == [("g", "chan")]

src/tests/django/test_context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from types import SimpleNamespace
2+
3+
from graphql_server.django.context import GraphQLDjangoContext
4+
5+
6+
def test_graphql_django_context_get_and_item_access():
7+
req = SimpleNamespace()
8+
res = SimpleNamespace()
9+
ctx = GraphQLDjangoContext(req, res)
10+
assert ctx["request"] is req
11+
assert ctx.get("response") is res

src/tests/http/test_base_view.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import json
2+
3+
from graphql_server.http.base import BaseView
4+
5+
6+
class DummyView(BaseView):
7+
graphql_ide = None
8+
9+
10+
def test_parse_query_params_extensions():
11+
view = DummyView()
12+
params = view.parse_query_params({"extensions": json.dumps({"a": 1})})
13+
assert params["extensions"] == {"a": 1}
14+
15+
16+
def test_is_multipart_subscriptions_boundary_check():
17+
view = DummyView()
18+
assert not view._is_multipart_subscriptions(
19+
"multipart/mixed", {"boundary": "notgraphql"}
20+
)

src/tests/test/test_client_utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import json
2+
import types
3+
4+
import pytest
5+
6+
from graphql_server.test.client import BaseGraphQLTestClient
7+
8+
9+
class DummyClient(BaseGraphQLTestClient):
10+
def request(self, body, headers=None, files=None):
11+
return types.SimpleNamespace(content=json.dumps(body).encode(), json=lambda: body)
12+
13+
14+
def test_build_body_with_variables_and_files():
15+
client = DummyClient(None)
16+
variables = {"files": [None, None], "textFile": None, "other": "x"}
17+
files = {"file1": object(), "file2": object(), "textFile": object()}
18+
body = client._build_body("query", variables, files)
19+
mapping = json.loads(body["map"])
20+
assert mapping == {
21+
"file1": ["variables.files.0"],
22+
"file2": ["variables.files.1"],
23+
"textFile": ["variables.textFile"],
24+
}
25+
26+
27+
def test_decode_multipart():
28+
client = DummyClient(None)
29+
response = types.SimpleNamespace(content=json.dumps({"a": 1}).encode())
30+
assert client._decode(response, type="multipart") == {"a": 1}
31+
32+
33+
def test_query_deprecated_arg_and_assertion():
34+
client = DummyClient(None)
35+
with pytest.deprecated_call():
36+
resp = client.query("{a}", asserts_errors=False)
37+
assert resp.errors is None

0 commit comments

Comments
 (0)