|
2 | 2 | import json |
3 | 3 | from typing import Mapping |
4 | 4 |
|
| 5 | +import aiohttp |
5 | 6 | import pytest |
6 | 7 |
|
7 | 8 | from gql import Client, gql |
| 9 | +from gql.graphql_request import GraphQLRequest |
8 | 10 | from gql.transport.exceptions import ( |
9 | 11 | TransportAlreadyConnected, |
10 | 12 | TransportClosed, |
| 13 | + TransportConnectionFailed, |
11 | 14 | TransportProtocolError, |
12 | 15 | TransportServerError, |
13 | 16 | ) |
@@ -243,6 +246,7 @@ async def test_http_multipart_graphql_errors(aiohttp_server): |
243 | 246 | """Test handling of GraphQL-level errors in response.""" |
244 | 247 | from aiohttp import web |
245 | 248 |
|
| 249 | + from gql.transport.exceptions import TransportQueryError |
246 | 250 | from gql.transport.http_multipart_transport import HTTPMultipartTransport |
247 | 251 |
|
248 | 252 | async def handler(request): |
@@ -275,15 +279,15 @@ async def handler(request): |
275 | 279 | async with Client(transport=transport) as session: |
276 | 280 | query = gql(subscription_str) |
277 | 281 |
|
278 | | - results = [] |
279 | | - async for result in session.subscribe(query): |
280 | | - results.append(result) |
| 282 | + # Client raises TransportQueryError when there are errors in the result |
| 283 | + with pytest.raises(TransportQueryError) as exc_info: |
| 284 | + async for result in session.subscribe(query): |
| 285 | + pass |
281 | 286 |
|
282 | | - assert len(results) == 1 |
283 | | - assert results[0]["book"]["title"] == "Book 1" |
284 | | - assert results[0].errors is not None |
285 | | - assert len(results[0].errors) == 1 |
286 | | - assert "deprecated" in results[0].errors[0]["message"] |
| 287 | + # Verify error details |
| 288 | + assert "deprecated" in str(exc_info.value).lower() |
| 289 | + assert exc_info.value.data is not None |
| 290 | + assert exc_info.value.data["book"]["title"] == "Book 1" |
287 | 291 |
|
288 | 292 |
|
289 | 293 | @pytest.mark.asyncio |
@@ -369,9 +373,10 @@ async def test_http_multipart_transport_not_connected(): |
369 | 373 | transport = HTTPMultipartTransport(url="http://example.com/graphql") |
370 | 374 |
|
371 | 375 | query = gql(subscription_str) |
| 376 | + request = GraphQLRequest(query) |
372 | 377 |
|
373 | 378 | with pytest.raises(TransportClosed): |
374 | | - async for result in transport.subscribe(query._ast): |
| 379 | + async for result in transport.subscribe(request): |
375 | 380 | pass |
376 | 381 |
|
377 | 382 |
|
@@ -492,3 +497,131 @@ async def handler(request): |
492 | 497 | results.append(result) |
493 | 498 |
|
494 | 499 | assert len(results) == 1 |
| 500 | + |
| 501 | + |
| 502 | +@pytest.mark.asyncio |
| 503 | +async def test_http_multipart_execute_empty_response(aiohttp_server): |
| 504 | + """Test execute method with empty response (no results).""" |
| 505 | + from aiohttp import web |
| 506 | + |
| 507 | + from gql.transport.http_multipart_transport import HTTPMultipartTransport |
| 508 | + |
| 509 | + async def handler(request): |
| 510 | + # Return empty multipart response (no data parts) |
| 511 | + body = "--graphql--\r\n" |
| 512 | + return web.Response( |
| 513 | + text=body, |
| 514 | + content_type='multipart/mixed; boundary="graphql"', |
| 515 | + ) |
| 516 | + |
| 517 | + app = web.Application() |
| 518 | + app.router.add_route("POST", "/", handler) |
| 519 | + server = await aiohttp_server(app) |
| 520 | + |
| 521 | + url = server.make_url("/") |
| 522 | + transport = HTTPMultipartTransport(url=url, timeout=10) |
| 523 | + |
| 524 | + async with Client(transport=transport) as session: |
| 525 | + query = gql(subscription_str) |
| 526 | + |
| 527 | + with pytest.raises(TransportProtocolError) as exc_info: |
| 528 | + await session.execute(query) |
| 529 | + |
| 530 | + assert "No result received" in str(exc_info.value) |
| 531 | + |
| 532 | + |
| 533 | +@pytest.mark.asyncio |
| 534 | +async def test_http_multipart_response_without_payload_wrapper(aiohttp_server): |
| 535 | + """Test parsing response without payload wrapper (direct format).""" |
| 536 | + from aiohttp import web |
| 537 | + |
| 538 | + from gql.transport.http_multipart_transport import HTTPMultipartTransport |
| 539 | + |
| 540 | + async def handler(request): |
| 541 | + # Send data in direct format (no payload wrapper) |
| 542 | + response = {"data": {"book": book1}} |
| 543 | + part = ( |
| 544 | + f"--graphql\r\n" |
| 545 | + f"Content-Type: application/json\r\n" |
| 546 | + f"\r\n" |
| 547 | + f"{json.dumps(response)}\r\n" |
| 548 | + f"--graphql--\r\n" |
| 549 | + ) |
| 550 | + return web.Response( |
| 551 | + text=part, |
| 552 | + content_type='multipart/mixed; boundary="graphql"', |
| 553 | + ) |
| 554 | + |
| 555 | + app = web.Application() |
| 556 | + app.router.add_route("POST", "/", handler) |
| 557 | + server = await aiohttp_server(app) |
| 558 | + |
| 559 | + url = server.make_url("/") |
| 560 | + transport = HTTPMultipartTransport(url=url, timeout=10) |
| 561 | + |
| 562 | + async with Client(transport=transport) as session: |
| 563 | + query = gql(subscription_str) |
| 564 | + |
| 565 | + results = [] |
| 566 | + async for result in session.subscribe(query): |
| 567 | + results.append(result) |
| 568 | + |
| 569 | + assert len(results) == 1 |
| 570 | + assert results[0]["book"]["title"] == "Book 1" |
| 571 | + |
| 572 | + |
| 573 | +@pytest.mark.asyncio |
| 574 | +async def test_http_multipart_newline_separator(aiohttp_server): |
| 575 | + """Test parsing multipart response with LF separator instead of CRLF.""" |
| 576 | + from aiohttp import web |
| 577 | + |
| 578 | + from gql.transport.http_multipart_transport import HTTPMultipartTransport |
| 579 | + |
| 580 | + async def handler(request): |
| 581 | + # Use LF instead of CRLF |
| 582 | + payload = {"data": {"book": book1}} |
| 583 | + wrapped = {"payload": payload} |
| 584 | + part = ( |
| 585 | + f"--graphql\n" |
| 586 | + f"Content-Type: application/json\n" |
| 587 | + f"\n" |
| 588 | + f"{json.dumps(wrapped)}\n" |
| 589 | + f"--graphql--\n" |
| 590 | + ) |
| 591 | + return web.Response( |
| 592 | + text=part, |
| 593 | + content_type='multipart/mixed; boundary="graphql"', |
| 594 | + ) |
| 595 | + |
| 596 | + app = web.Application() |
| 597 | + app.router.add_route("POST", "/", handler) |
| 598 | + server = await aiohttp_server(app) |
| 599 | + |
| 600 | + url = server.make_url("/") |
| 601 | + transport = HTTPMultipartTransport(url=url, timeout=10) |
| 602 | + |
| 603 | + async with Client(transport=transport) as session: |
| 604 | + query = gql(subscription_str) |
| 605 | + |
| 606 | + results = [] |
| 607 | + async for result in session.subscribe(query): |
| 608 | + results.append(result) |
| 609 | + |
| 610 | + assert len(results) == 1 |
| 611 | + assert results[0]["book"]["title"] == "Book 1" |
| 612 | + |
| 613 | + |
| 614 | +@pytest.mark.asyncio |
| 615 | +async def test_http_multipart_connection_error(): |
| 616 | + """Test handling of connection errors (non-transport exceptions).""" |
| 617 | + from gql.transport.http_multipart_transport import HTTPMultipartTransport |
| 618 | + |
| 619 | + # Use an invalid URL that will fail to connect |
| 620 | + transport = HTTPMultipartTransport(url="http://invalid.local:99999/graphql", timeout=1) |
| 621 | + |
| 622 | + async with Client(transport=transport) as session: |
| 623 | + query = gql(subscription_str) |
| 624 | + |
| 625 | + with pytest.raises(TransportConnectionFailed): |
| 626 | + async for result in session.subscribe(query): |
| 627 | + pass |
0 commit comments