async def test_connection_error(): """Test that server responds correctly when connection errors happen. Check that server responds with message of type `connection_error` when there was an exception in `on_connect` method. """ print("Prepare application.") class MyGraphqlWsConsumerConnectionError(GraphqlWsConsumer): """Channels WebSocket consumer which provides GraphQL API.""" schema = "" async def on_connect(self, payload): from graphql.error import GraphQLError # Always close the connection. raise GraphQLError("Reject connection") application = channels.routing.ProtocolTypeRouter( { "websocket": channels.routing.URLRouter( [ django.urls.path( "graphql-connection-error/", MyGraphqlWsConsumerConnectionError ) ] ) } ) # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator( application=application, path="graphql-connection-error/", subprotocols=["graphql-ws"], ) print("Try to initialize the connection.") await comm.connect(timeout=TIMEOUT) await comm.send_json_to({"type": "connection_init", "payload": ""}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["type"] == "connection_error" assert resp["payload"]["message"] == "Reject connection" resp = await comm.receive_output(timeout=TIMEOUT) assert resp["type"] == "websocket.close" assert resp["code"] == 4000 print("Disconnect and wait the application to finish gracefully.") await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def test_keepalive(): """Test that server sends keepalive messages.""" print("Prepare application.") class MyGraphqlWsConsumerKeepalive(GraphqlWsConsumer): """Channels WebSocket consumer which provides GraphQL API.""" schema = "" # Period to send keepalive mesages. Just some reasonable number. send_keepalive_every = 0.05 application = channels.routing.ProtocolTypeRouter( { "websocket": channels.routing.URLRouter( [django.urls.path("graphql-keepalive/", MyGraphqlWsConsumerKeepalive)] ) } ) # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator( application=application, path="graphql-keepalive/", subprotocols=["graphql-ws"] ) print("Establish & initialize the connection.") await comm.connect(timeout=TIMEOUT) await comm.send_json_to({"type": "connection_init", "payload": ""}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["type"] == "connection_ack" resp = await comm.receive_json_from(timeout=TIMEOUT) assert ( resp["type"] == "ka" ), "Keepalive message expected right after `connection_ack`!" print("Receive several keepalive messages.") pings = [] for _ in range(3): pings.append(await comm.receive_json_from(timeout=TIMEOUT)) assert all([ping["type"] == "ka" for ping in pings]) print("Send connection termination message.") await comm.send_json_to({"type": "connection_terminate"}) print("Disconnect and wait the application to finish gracefully.") await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def create_and_subscribe(userId): """Establish and initialize WebSocket GraphQL connection. Subscribe to GraphQL subscription by userId. Args: userId: User ID for `on_chat_message_sent` subscription. Returns: sub_id: Subscription uid. comm: Client, instance of the `WebsocketCommunicator`. """ comm = ch_testing.WebsocketCommunicator( application=my_app, path="graphql/", subprotocols=["graphql-ws"] ) await comm.connect(timeout=TIMEOUT) await comm.send_json_to({"type": "connection_init", "payload": ""}) await comm.receive_json_from(timeout=TIMEOUT) sub_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": sub_id, "type": "start", "payload": { "query": textwrap.dedent( """ subscription MyOperationName($userId: UserId) { on_chat_message_sent(userId: $userId) { event } } """ ), "variables": {"userId": userId}, "operationName": "MyOperationName", }, } ) return sub_id, comm
async def test_main_usecase(): """Test main use-case with the GraphQL over WebSocket.""" # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator( application=my_app, path="graphql/", subprotocols=["graphql-ws"] ) print("Establish WebSocket connection and check a subprotocol.") connected, subprotocol = await comm.connect(timeout=TIMEOUT) assert connected, "Could not connect to the GraphQL subscriptions WebSocket!" assert subprotocol == "graphql-ws", "Wrong subprotocol received!" print("Initialize GraphQL connection.") await comm.send_json_to({"type": "connection_init", "payload": ""}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["type"] == "connection_ack" print("Make simple GraphQL query and check the response.") uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ query MyOperationName { value } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" assert "errors" not in resp["payload"] assert resp["payload"]["data"]["value"] == MyQuery.VALUE resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print("Subscribe to GraphQL subscription.") subscription_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": subscription_id, "type": "start", "payload": { "query": textwrap.dedent( """ subscription MyOperationName { on_chat_message_sent(userId: ALICE) { event } } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) print("Trigger the subscription by mutation to receive notification.") uniq_id = str(uuid.uuid4().hex) message = "Hi!" await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ mutation MyOperationName($message: String!) { send_chat_message(message: $message) { message } } """ ), "variables": {"message": message}, "operationName": "MyOperationName", }, } ) # Mutation response. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" assert "errors" not in resp["payload"] assert resp["payload"]["data"] == {"send_chat_message": {"message": message}} resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" # Subscription notification. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == subscription_id, "Notification id != subscription id!" assert resp["type"] == "data", "Type `data` expected!" assert "errors" not in resp["payload"] event = resp["payload"]["data"]["on_chat_message_sent"]["event"] assert json.loads(event) == { "userId": UserId.ALICE, "payload": message, }, "Subscription notification contains wrong data!" print("Disconnect and wait the application to finish gracefully.") await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def test_subscribe_unsubscribe(): """Test subscribe-unsubscribe behavior with the GraphQL over WebSocket. 0. Subscribe to GraphQL subscription: messages for Alice. 1. Send STOP message and unsubscribe. 2. Subscribe to GraphQL subscription: messages for Tom. 3. Call unsubscribe method of the Subscription instance (via `kick_out_user` mutation). 4. Execute some mutation. 5. Check subscription notifications: there are no notifications. """ # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator( application=my_app, path="graphql/", subprotocols=["graphql-ws"] ) print("Establish and initialize WebSocket GraphQL connection.") await comm.connect(timeout=TIMEOUT) await comm.send_json_to({"type": "connection_init", "payload": ""}) await comm.receive_json_from(timeout=TIMEOUT) print("Subscribe to GraphQL subscription.") sub_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": sub_id, "type": "start", "payload": { "query": textwrap.dedent( """ subscription MyOperationName { on_chat_message_sent(userId: ALICE) { event } } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) print("Stop subscription by id.") await comm.send_json_to({"id": sub_id, "type": "stop"}) # Subscription notification with unsubscribe information. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == sub_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print("Subscribe to GraphQL subscription.") sub_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": sub_id, "type": "start", "payload": { "query": textwrap.dedent( """ subscription MyOperationName { on_chat_message_sent(userId: TOM) { event } } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) print("Stop all subscriptions for TOM.") uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ mutation MyOperationName { kick_out_user(userId: TOM) { success } } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) # Mutation response. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" # Subscription notification with unsubscribe information. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == sub_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print("Trigger the subscription by mutation to receive notification.") uniq_id = str(uuid.uuid4().hex) message = "Is anybody here?" await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ mutation MyOperationName($message: String!) { send_chat_message(message: $message) { message } } """ ), "variables": {"message": message}, "operationName": "MyOperationName", }, } ) # Mutation response. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" # Check notifications: there are no notifications! Previously, # we have unsubscribed from all subscriptions. assert await comm.receive_nothing() is True, "No notifications expected!" print("Disconnect and wait the application to finish gracefully.") await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def test_error_cases(): """Test that server responds correctly when errors happen. Check that server responds with message of type `data` when there is a syntax error in the request or the exception in a resolver was raised. Check that server responds with message of type `error` when there was an exceptional situation, for example, field `query` of `payload` is missing or field `type` has a wrong value. """ # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator( application=my_app, path="graphql/", subprotocols=["graphql-ws"] ) print("Establish & initialize the connection.") await comm.connect(timeout=TIMEOUT) await comm.send_json_to({"type": "connection_init", "payload": ""}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["type"] == "connection_ack" print("Check that query syntax error leads to the `error` response.") uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "wrong_type__(ツ)_/¯", "payload": {"variables": {}, "operationName": "MyOperationName"}, } ) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "error", "Type `error` expected!" assert len(resp["payload"]) == 1, "Single error expected!" assert isinstance( resp["payload"]["errors"][0], str ), "Error must be of string type!" print( "Check that query syntax error leads to the `data` response " "with `errors` array." ) uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ This list produces a syntax error! """ ), "variables": {}, "operationName": "MyOperationName", }, } ) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" payload = resp["payload"] assert payload["data"] is None errors = payload["errors"] assert len(errors) == 1, "Single error expected!" assert ( "message" in errors[0] and "locations" in errors[0] ), "Response missing mandatory fields!" assert errors[0]["locations"] == [{"line": 1, "column": 1}] resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print( "Check that query syntax error leads to the `data` response " "with `errors` array." ) uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ query MyOperationName { value(issue_error: true) } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" payload = resp["payload"] assert payload["data"]["value"] is None errors = payload["errors"] assert len(errors) == 1, "Single error expected!" assert errors[0]["message"] == MyQuery.VALUE assert "locations" in errors[0] resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print("Check multiple errors in the `data` message.") uniq_id = str(uuid.uuid4().hex) await comm.send_json_to( { "id": uniq_id, "type": "start", "payload": { "query": textwrap.dedent( """ query { projects { path wrong_filed } } query a { projects } { wrong_name } """ ), "variables": {}, "operationName": "MyOperationName", }, } ) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "data", "Type `data` expected!" payload = resp["payload"] assert payload["data"] is None errors = payload["errors"] assert len(errors) == 5, "Five errors expected!" # Message is here: `This anonymous operation must be # the only defined operation`. assert errors[0]["message"] == errors[3]["message"] assert "locations" in errors[2], "The `locations` field expected" assert "locations" in errors[4], "The `locations` field expected" resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp["id"] == uniq_id, "Response id != request id!" assert resp["type"] == "complete", "Type `complete` expected!" print("Disconnect and wait the application to finish gracefully.") await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def test_main_usecase(): """Test main use-case with the GraphQL over WebSocket.""" # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator(application=my_app, path='graphql/', subprotocols='graphql-ws') print('Establish WebSocket connection and check a subprotocol.') connected, subprotocol = await comm.connect(timeout=TIMEOUT) assert connected, ('Could not connect to the GraphQL subscriptions ' 'WebSocket!') assert subprotocol == 'graphql-ws', 'Wrong subprotocol received!' print('Initialize GraphQL connection.') await comm.send_json_to({'type': 'connection_init', 'payload': ''}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['type'] == 'connection_ack' print('Make simple GraphQL query and check the response.') uniq_id = str(uuid.uuid4().hex) await comm.send_json_to({ 'id': uniq_id, 'type': 'start', 'payload': { 'query': textwrap.dedent(''' query MyOperationName { value } '''), 'variables': {}, 'operationName': 'MyOperationName', } }) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'data', 'Type `data` expected!' assert 'errors' not in resp['payload'] assert resp['payload']['data']['value'] == MyQuery.VALUE resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'complete', 'Type `complete` expected!' print('Subscribe to GraphQL subscription.') uniq_id = str(uuid.uuid4().hex) sub_id = uniq_id sub_param1 = str(uuid.uuid4().hex) sub_param2 = str(uuid.uuid4().hex) await comm.send_json_to({ 'id': uniq_id, 'type': 'start', 'payload': { 'query': textwrap.dedent(''' subscription MyOperationName($sub_param1: String, $sub_param2: String) { my_sub(param1: $sub_param1, param2: $sub_param2) { event } } '''), 'variables': { 'sub_param1': sub_param1, 'sub_param2': sub_param2 }, 'operationName': 'MyOperationName', } }) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'data', 'Type `data` expected!' assert 'errors' not in resp['payload'] assert resp['payload']['data'] == {} print('Trigger the subscription by mutation to receive notification.') uniq_id = str(uuid.uuid4().hex) await comm.send_json_to({ 'id': uniq_id, 'type': 'start', 'payload': { 'query': textwrap.dedent(''' mutation MyOperationName($mut_param: String) { my_mutation(param: $mut_param, cmd: BROADCAST) { ok } } '''), 'variables': { 'mut_param': sub_param1 }, 'operationName': 'MyOperationName', } }) # Mutation response. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'data', 'Type `data` expected!' assert 'errors' not in resp['payload'] assert resp['payload']['data'] == {'my_mutation': {'ok': True}} resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'complete', 'Type `complete` expected!' # Subscription notification. resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == sub_id, ('Notification id != subscription id!') assert resp['type'] == 'data', 'Type `data` expected!' assert 'errors' not in resp['payload'] assert json.loads(resp['payload']['data']['my_sub']['event']) == { 'value': MySubscription.VALUE, 'sub_param1': sub_param1, 'sub_param2': sub_param2, 'payload': MyMutation.PAYLOAD, }, 'Subscription notification contains wrong data!' print('Disconnect and wait the application to finish gracefully.') await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)
async def test_error_cases(): """Test that server responds correctly when errors happen. Check that server responds with message of type `error` when there is a syntax error in the request, but `data` with `errors` field responds to the exception in a resolver. """ # Channels communicator to test WebSocket consumers. comm = ch_testing.WebsocketCommunicator(application=my_app, path='graphql/', subprotocols='graphql-ws') print('Establish & initialize the connection.') await comm.connect(timeout=TIMEOUT) await comm.send_json_to({'type': 'connection_init', 'payload': ''}) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['type'] == 'connection_ack' print('Check that query syntax error leads to the `error` response.') uniq_id = str(uuid.uuid4().hex) await comm.send_json_to({ 'id': uniq_id, 'type': 'start', 'payload': { 'query': textwrap.dedent(''' This list produces a syntax error! '''), 'variables': {}, 'operationName': 'MyOperationName', } }) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'error', 'Type `error` expected!' payload = resp['payload'] assert ('message' in payload and 'locations' in payload), 'Response missing mandatory fields!' assert len(payload) == 2, 'Extra fields detected in the response!' print('Check that resolver error leads to the `data` message.') uniq_id = str(uuid.uuid4().hex) await comm.send_json_to({ 'id': uniq_id, 'type': 'start', 'payload': { 'query': textwrap.dedent(''' query MyOperationName { value(issue_error: true) } '''), 'variables': {}, 'operationName': 'MyOperationName', } }) resp = await comm.receive_json_from(timeout=TIMEOUT) assert resp['id'] == uniq_id, 'Response id != request id!' assert resp['type'] == 'data', 'Type `data` expected!' payload = resp['payload'] assert payload['data']['value'] is None errors = payload['errors'] assert len(errors) == 1, 'Single error expected!' assert errors[0]['message'] == MyQuery.VALUE assert 'locations' in errors[0] print('Disconnect and wait the application to finish gracefully.') await comm.disconnect(timeout=TIMEOUT) await comm.wait(timeout=TIMEOUT)