Passed
Push — main ( 40faed...b2501b )
by Eran
01:33
created

test_graphql.SendStub.__call__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
from collections.abc import Generator
2
from typing import Any
3
from unittest.mock import MagicMock, patch
4
5
import pytest
6
import strawberry
7
from starlette.requests import Request
8
from starlette.testclient import TestClient
9
10
from graphinate.renderers import graphql
11
from graphinate.renderers.graphql import (
12
    GRAPHQL_ROUTE_PATH,
13
    _graphql_app,
14
    _openapi_schema,
15
    _starlette_app,
16
)
17
18
# region --- Helper Classes ---
19
20
21
@strawberry.type
22
class Query:
23
    """A simple GraphQL Query type for testing purposes."""
24
25
    @strawberry.field
26
    def hello(self) -> str:
27
        """A simple test field."""
28
        return "world"
29
30
31
# Create a fake schema for testing
32
FAKE_SCHEMA = strawberry.Schema(query=Query)
33
34
35
class ReceiveStub:
36
    """A ASGI receive callable that does nothing."""
37
38
    async def __call__(self):
39
        return {}
40
41
42
class SendStub:
43
    """A ASGI send callable that does nothing."""
44
45
    async def __call__(self, message):
46
        pass
47
48
49
class RequestStub(Request):
50
    """A Starlette Request object for testing purposes."""
51
52
    def __init__(self, method="GET", path="/openapi.json", query_string=b""):
53
        scope = {
54
            "type": "http",
55
            "asgi.version": "3.0",
56
            "asgi.spec_version": "2.1",
57
            "method": method,
58
            "path": path,
59
            "raw_path": path.encode(),
60
            "query_string": query_string,
61
            "headers": [],
62
            "client": ("testclient", 1234),
63
            "server": ("testserver", 80),
64
            "scheme": "http",
65
            "root_path": "",
66
            "app": None,
67
        }
68
        super().__init__(scope, ReceiveStub())
69
70
71
# endregion --- Helper Classes ---
72
73
# region --- Pytest Fixtures ---
74
75
@pytest.fixture
76
def client() -> Generator[TestClient, Any, None]:
77
    """
78
    Pytest fixture that creates a TestClient for the full Starlette app
79
    with a dummy GraphQL schema.
80
    """
81
    # Arrange
82
    graphql_app = _graphql_app(FAKE_SCHEMA)
83
    app = _starlette_app(graphql_app)
84
    with TestClient(app) as test_client:
85
        yield test_client
86
87
88
@pytest.fixture
89
def fake_schema():
90
    return FAKE_SCHEMA
91
92
93
@pytest.fixture
94
def request_stub():
95
    return RequestStub()
96
97
98
# endregion --- Pytest Fixtures ---
99
100
# region --- Test Cases ---
101
102
def test_graphql_query_success(client: TestClient):
103
    """
104
    Tests a successful GraphQL query to the /graphql endpoint.
105
    """
106
    # Arrange
107
    query = {"query": "query TestHello { hello }"}
108
109
    # Act
110
    response = client.post(GRAPHQL_ROUTE_PATH, json=query)
111
112
    # Assert
113
    assert response.status_code == 200
114
    assert response.json() == {"data": {"hello": "world"}}
115
116
117
def test_graphql_query_error_gracefully(client: TestClient):
118
    """
119
    Tests that an invalid GraphQL query is handled gracefully.
120
    """
121
    # Arrange
122
    query = {"query": "{ nonExistentField }"}
123
124
    # Act
125
    response = client.post(GRAPHQL_ROUTE_PATH, json=query)
126
127
    # Assert
128
    # The server should return 200 OK, but the body will contain an 'errors' key.
129
    assert response.status_code == 200
130
    response_data = response.json()
131
    assert "data" in response_data
132
    assert response_data["data"] is None
133
    assert "errors" in response_data
134
    assert len(response_data["errors"]) > 0
135
    assert "nonExistentField" in response_data["errors"][0]["message"]
136
137
138
def test_metrics_endpoint(client: TestClient):
139
    """
140
    Tests that the /metrics endpoint is available and serves Prometheus metrics.
141
    """
142
    # Arrange (client is provided by the fixture)
143
144
    # Act
145
    response = client.get("/metrics")
146
147
    # Assert
148
    assert response.status_code == 200
149
    # Check for a known metric provided by starlette-prometheus
150
    assert "starlette_requests_total" in response.text
151
152
153
@patch("graphinate.renderers.graphql.webbrowser.open")
154
def test_browse_flag_opens_browser(mock_webbrowser_open: MagicMock):
155
    """
156
    Ensures the browser is opened on startup when the 'browse' kwarg is True.
157
    """
158
    # Arrange
159
    # The lifespan event that opens the browser is triggered when the TestClient
160
    # is used as a context manager.
161
    app = _starlette_app(port=1234, browse=True)
162
163
    # Act
164
    with TestClient(app):
165
        pass  # Startup runs, then shutdown
166
167
    # Assert
168
    mock_webbrowser_open.assert_called_once_with('http://localhost:1234/viewer')
169
170
171
def test_schema_generator_or_response_error_handling(monkeypatch, request_stub):
172
    from graphinate.renderers import graphql as graphql_mod
173
174
    # mock strawberry-graphql schema module
175
    class FailingSchemaGenerator:
176
        def __init__(self, data):
177
            pass
178
179
        def OpenAPIResponse(self, request):  # noqa: N802
180
            raise RuntimeError("Schema generation failed")
181
182
    monkeypatch.setattr(graphql_mod, "SchemaGenerator", FailingSchemaGenerator)
183
    with pytest.raises(RuntimeError):
184
        _openapi_schema(request_stub)
185
186
187
def test_server_missing_required_arguments():
188
    # server requires at least graphql_schema
189
    with pytest.raises(TypeError):
190
        graphql.server()
191
192
193
def test_server_uvicorn_import_or_runtime_error(mocker, fake_schema):
194
    mocker.patch("graphinate.renderers.graphql._graphql_app", autospec=True)
195
    mocker.patch("graphinate.renderers.graphql._starlette_app", autospec=True)
196
    import builtins
197
    original_import = builtins.__import__
198
199
    def import_side_effect(name, *args, **kwargs):
200
        if name == "uvicorn":
201
            raise ImportError("No module named 'uvicorn'")
202
        return original_import(name, *args, **kwargs)
203
204
    mocker.patch("builtins.__import__", side_effect=import_side_effect)
205
    with pytest.raises(ImportError, match="No module named 'uvicorn'"):
206
        graphql.server(fake_schema)
207
208
209
def test_server_prometheus_middleware_integration():
210
    # We'll check that PrometheusMiddleware is added to the app
211
212
    # Arrange
213
    graphql_app = MagicMock()
214
215
    # Act
216
    app = graphql._starlette_app(graphql_app)
217
    middleware_names = [mw.cls.__name__ for mw in app.user_middleware]
218
    client = TestClient(app)
219
    response = client.get("/metrics")
220
221
    # Assert
222
    assert "PrometheusMiddleware" in middleware_names
223
    assert response.status_code == 200
224
225
# endregion --- Test Cases ---
226