Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include "qiopipe.h"
++
++ #include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
++ #include <QtLanguageServer/private/qlanguageserverjsonrpctransport_p.h>
++ #include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
++
++ #include <QtCore/qjsonarray.h>
++ #include <QtCore/qjsonobject.h>
++ #include <QtCore/qjsondocument.h>
++ #include <QtCore/qstring.h>
++ #include <QtTest/qsignalspy.h>
++ #include <QtTest/qtest.h>
++
++ using namespace Qt::StringLiterals;
++ using namespace QLspSpecification;
++
++ class TestRigEventHandler
++ {
++ public:
++ TestRigEventHandler(QLanguageServerProtocol *protocol, QIODevice *device) : m_device(device)
++ {
++ protocol->registerInitializeRequestHandler(
++ [this](const QByteArray &, const InitializeParams &clientCapabilities,
++ auto &&response) {
++ Q_UNUSED(clientCapabilities);
++ m_isInitialized = true;
++ InitializeResult result;
++ result.capabilities = m_serverCapabilities;
++ response.sendResponse(result);
++ });
++ protocol->registerInitializedNotificationHandler(
++ [](const QByteArray &, const InitializedParams &) {});
++ protocol->registerShutdownRequestHandler(
++ [](const QByteArray &, const auto &, auto &&response) { response.sendResponse(); });
++ protocol->registerExitNotificationHandler(
++ [this](const QByteArray &, const auto &) { m_hasExited = true; });
++
++ // TODO: There should be a way to do this without using the typed RPC layer.
++ protocol->typedRpc()->installMessagePreprocessor(
++ [this](const QJsonDocument &doc, const QJsonParseError &,
++ const QJsonRpcProtocol::Handler<QJsonRpcProtocol::Response> &responder) {
++ if (m_isInitialized)
++ return QJsonRpcProtocol::Processing::Continue;
++
++ if (doc.object()[u"method"].toString()
++ == QString::fromUtf8(QLspSpecification::Requests::InitializeMethod)) {
++ return QJsonRpcProtocol::Processing::Continue;
++ }
++
++ responder(QJsonRpcProtocol::MessageHandler::error(
++ int(QLspSpecification::ErrorCodes::ServerNotInitialized),
++ u"Method called on non initialized Language Server"_s));
++ return QJsonRpcProtocol::Processing::Stop;
++ });
++
++ QObject::connect(device, &QIODevice::readyRead, m_device,
++ [this, protocol]() { protocol->receiveData(m_device->readAll()); });
++ }
++
++ bool isInitialized() const { return m_isInitialized; }
++ bool hasExited() const { return m_hasExited; }
++
++ void setServerCapabilities(QLspSpecification::ServerCapabilities capabilities)
++ {
++ m_serverCapabilities = std::move(capabilities);
++ }
++
++ QLspSpecification::ServerCapabilities serverCapabilities() const
++ {
++ return m_serverCapabilities;
++ }
++
++ private:
++ QLspSpecification::ServerCapabilities m_serverCapabilities;
++ QIODevice *m_device = nullptr;
++ bool m_isInitialized = false;
++ bool m_hasExited = false;
++ };
++
++ static void setTransportDevice(QLanguageServerJsonRpcTransport *transport, QIODevice *device)
++ {
++ transport->setDataHandler([device](const QByteArray &data) {
++ device->write(data);
++ });
++
++ QObject::connect(device, &QIODevice::readyRead, device, [device, transport]() {
++ transport->receiveData(device->readAll());
++ });
++ }
++
++ static void sendAndWaitJsonRpc(QJsonRpcProtocol *protocol,
++ const QJsonRpcProtocol::Request &request,
++ const QJsonRpcProtocol::Handler<QJsonRpcProtocol::Response> &handler)
++ {
++ bool handled = false;
++ protocol->sendRequest(request, [&](const QJsonRpcProtocol::Response &response) {
++ handled = true;
++ handler(response);
++ });
++ QTRY_VERIFY(handled);
++ }
++
++ class TestRig
++ {
++ public:
++ QLanguageServerProtocol protocol;
++ QJsonRpcProtocol client;
++ std::unique_ptr<TestRigEventHandler> handler;
++
++ TestRig() : protocol([this](const QByteArray &data) { pipe.end1()->write(data); })
++ {
++ handler = std::make_unique<TestRigEventHandler>(&protocol, pipe.end1());
++ setTransportDevice(&clientTransport, pipe.end2());
++ client.setTransport(&clientTransport);
++ }
++
++ void open(const QLspSpecification::ServerCapabilities &capabilities
++ = QLspSpecification::ServerCapabilities())
++ {
++ handler->setServerCapabilities(capabilities);
++ QVERIFY(pipe.open(QIODevice::ReadWrite));
++ }
++
++ void initialize(const QLspSpecification::ClientCapabilities &clientCapabilities
++ = QLspSpecification::ClientCapabilities())
++ {
++ QLspSpecification::InitializeParams params;
++ params.capabilities = clientCapabilities;
++ sendAndWaitJsonRpc(&client, QJsonRpcProtocol::Request {
++ 0,
++ "initialize",
++ QTypedJson::toJsonValue(params)
++ }, [&](const QJsonRpcProtocol::Response &response) {
++ QCOMPARE(response.data["capabilities"],
++ QTypedJson::toJsonValue(handler->serverCapabilities()));
++ });
++ }
++
++ private:
++ QLanguageServerJsonRpcTransport clientTransport;
++ QIOPipe pipe;
++ };
++
++ class tst_QLanguageServer : public QObject
++ {
++ Q_OBJECT
++
++ private slots:
++ void jsonRpcTransport();
++ void jsonRpcTransportHeaderCase();
++ void invalidHeaderField();
++ void protocolHandlesTransportErrors();
++ void invalidJson();
++ void protocolError();
++
++ void lifecycle();
++ void windowShowMessage();
++ void windowShowMessageRequest();
++ void windowLogMessage();
++ void telemetryEvent();
++
++ void clientRegisterCapability();
++ void setRequestHandler();
++
++ void rejectOutOfBoundMessages_data();
++ void rejectOutOfBoundMessages();
++
++ private:
++ void logOrShowMessage(const QString &method);
++ };
++
++ class NotificationHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ using Handler = std::function<void(QJsonRpcProtocol::Notification)>;
++
++ NotificationHandler(const Handler &handler) : m_notificationHandler(handler) {}
++
++ void handleRequest(const QJsonRpcProtocol::Request &request, const ResponseHandler &handler) final;
++ void handleNotification(const QJsonRpcProtocol::Notification ¬ification) final;
++
++ private:
++ Handler m_notificationHandler;
++ };
++
++ class RequestHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ using Handler = std::function<QJsonRpcProtocol::Response(QJsonRpcProtocol::Request)>;
++
++ RequestHandler(const Handler &handler) : m_requestHandler(handler) {}
++
++ void handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler) final;
++ void handleNotification(const QJsonRpcProtocol::Notification ¬ification) final;
++
++ private:
++ Handler m_requestHandler;
++ };
++
++ void NotificationHandler::handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &responseHandler)
++ {
++ Q_UNUSED(request);
++ Q_UNUSED(responseHandler);
++ QFAIL("request received when notification was expected");
++ }
++
++ void NotificationHandler::handleNotification(const QJsonRpcProtocol::Notification ¬ification)
++ {
++ m_notificationHandler(notification);
++ }
++
++
++ void RequestHandler::handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &responseHandler)
++ {
++ responseHandler(m_requestHandler(request));
++ }
++
++ void RequestHandler::handleNotification(const QJsonRpcProtocol::Notification ¬ification)
++ {
++ Q_UNUSED(notification);
++ QFAIL("notification received when request was expected");
++ }
++
++ void tst_QLanguageServer::jsonRpcTransport()
++ {
++ static const QByteArray header = "Content-Length: 15\r\n\r\n";
++ static const QByteArray content = "{\"some\":\"json\"}";
++
++ {
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::WriteOnly));
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end2());
++
++ int messages = 0;
++ transport.setMessageHandler([&](const QJsonDocument &message,
++ const QJsonParseError &error) {
++ QCOMPARE(error.error, QJsonParseError::NoError);
++ QCOMPARE(message.toJson(QJsonDocument::Compact), content);
++ ++messages;
++ });
++
++ for (int i = 0; i < 5; ++i) {
++ QCOMPARE(pipe.end1()->write(header), header.size());
++ QCOMPARE(pipe.end1()->write(content), content.size());
++
++ QTRY_COMPARE(messages, i + 1);
++ }
++ }
++
++ {
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::ReadOnly));
++
++ QByteArray received;
++ QIODevice *end1 = pipe.end1();
++ connect(end1, &QIODevice::readyRead, end1, [&]() {
++ received.append(end1->read(end1->bytesAvailable()));
++ });
++
++ QLanguageServerJsonRpcTransport protocol;
++ setTransportDevice(&protocol, pipe.end2());
++
++ for (int i = 0; i < 5; ++i) {
++ protocol.sendMessage(QJsonDocument::fromJson(content));
++ QTRY_COMPARE(received, header + content);
++ received.clear();
++ }
++ }
++ }
++
++ void tst_QLanguageServer::jsonRpcTransportHeaderCase()
++ {
++ static const QList<QByteArray> headers = { { "Content-Length: 15\r\n\r\n" },
++ { "content-length: 15\r\n\r\n" },
++ { "ConTEnt-lEngTh: 15 \r\n\r\n" },
++ { "CONTENT-LENGTH: 15\r\n\r\n" },
++ { "cONtENt-LENgTh: 15 \r\n\r\n" } };
++ static const QByteArray content = "{\"some\":\"json\"}";
++
++ {
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::WriteOnly));
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end2());
++
++ int messages = 0;
++ transport.setMessageHandler(
++ [&](const QJsonDocument &message, const QJsonParseError &error) {
++ QCOMPARE(error.error, QJsonParseError::NoError);
++ QCOMPARE(message.toJson(QJsonDocument::Compact), content);
++ ++messages;
++ });
++
++ for (int i = 0; i < 5; ++i) {
++ QCOMPARE(pipe.end1()->write(headers.at(i)), headers.at(i).size());
++ QCOMPARE(pipe.end1()->write(content), content.size());
++
++ QTRY_COMPARE(messages, i + 1);
++ }
++ }
++
++ {
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::ReadOnly));
++
++ QByteArray received;
++ QIODevice *end1 = pipe.end1();
++ connect(end1, &QIODevice::readyRead, end1,
++ [&]() { received.append(end1->read(end1->bytesAvailable())); });
++
++ QLanguageServerJsonRpcTransport protocol;
++ setTransportDevice(&protocol, pipe.end2());
++
++ for (int i = 0; i < 5; ++i) {
++ protocol.sendMessage(QJsonDocument::fromJson(content));
++ QTRY_COMPARE(received, headers.at(0) + content);
++ received.clear();
++ }
++ }
++ }
++
++ void tst_QLanguageServer::invalidHeaderField()
++ {
++ static const QByteArray header = "Broken-Mess\r\n"
++ "Content-Length: 15\r\n\r\n";
++ static const QByteArray content = "{\"some\":\"json\"}";
++
++ QIOPipe pipe;
++
++ QVERIFY(pipe.open(QIODevice::ReadWrite));
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end1());
++
++ int messages = 0;
++ transport.setMessageHandler([&](const QJsonDocument &message,
++ const QJsonParseError &error) {
++ QCOMPARE(error.error, QJsonParseError::NoError);
++ QCOMPARE(message.toJson(QJsonDocument::Compact), content);
++ ++messages;
++ });
++
++ int warnings = 0;
++ transport.setDiagnosticHandler([&](QJsonRpcTransport::DiagnosticLevel level,
++ const QString &message) {
++ QVERIFY2(level == QJsonRpcTransport::Warning, qPrintable(message));
++ ++warnings;
++ });
++
++ QIODevice *end2 = pipe.end2();
++ for (int i = 0; i < 5; ++i) {
++ QCOMPARE(end2->write(header), header.size());
++ QCOMPARE(end2->write(content), content.size());
++
++ QTRY_COMPARE(messages, i + 1);
++ QCOMPARE(warnings, 2 * (i + 1));
++ }
++ }
++
++ void tst_QLanguageServer::protocolHandlesTransportErrors()
++ {
++ static const QByteArray header = "Broken-Mess\r\n"
++ "Content-Length: 15\r\n\r\n";
++ static const QByteArray content = "{\"some\":\"json\"}";
++
++ QIOPipe pipe;
++
++ QVERIFY(pipe.open(QIODevice::ReadWrite));
++
++ auto device = pipe.end1();
++ QLanguageServerProtocol protocol([device](const QByteArray &data) { device->write(data); });
++
++ QObject::connect(device, &QIODevice::readyRead, device,
++ [device, &protocol]() { protocol.receiveData(device->readAll()); });
++
++ int warnings = 0;
++ protocol.registerResponseErrorHandler(
++ [&](const QLspSpecification::ResponseError &) { ++warnings; });
++
++ QIODevice *end2 = pipe.end2();
++ for (int i = 0; i < 5; ++i) {
++ QCOMPARE(end2->write(header), header.size());
++ QCOMPARE(end2->write(content), content.size());
++ QTRY_COMPARE(warnings, 2 * (i + 1));
++ }
++ }
++
++ void tst_QLanguageServer::invalidJson()
++ {
++ static const QByteArray header = "Content-Length: 15\r\n\r\n";
++ static const QByteArray content = "{\"someZ:\"json\"}";
++
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::ReadWrite));
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end2());
++
++ int messages = 0;
++ transport.setMessageHandler([&](const QJsonDocument &doc, const QJsonParseError &error) {
++ QVERIFY(error.error != QJsonParseError::NoError);
++ QVERIFY(doc.isNull());
++ ++messages;
++ });
++
++ QIODevice *end1 = pipe.end1();
++ for (int i = 0; i < 5; ++i) {
++ QCOMPARE(end1->write(header), header.size());
++ QCOMPARE(end1->write(content), content.size());
++ QTRY_COMPARE(messages, i + 1);
++ }
++ }
++
++ void tst_QLanguageServer::protocolError()
++ {
++ static const QByteArray header = "Total-Mess: schalala\r\n\r\n";
++
++ QIOPipe pipe;
++ QVERIFY(pipe.open(QIODevice::WriteOnly));
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end2());
++
++ int errors = 0;
++ transport.setDiagnosticHandler([&](QJsonRpcTransport::DiagnosticLevel level, const QString &) {
++ if (level == QJsonRpcTransport::Error)
++ ++errors;
++ });
++
++ int messages = 0;
++ transport.setMessageHandler([&](const QJsonDocument &doc, const QJsonParseError &error) {
++ Q_UNUSED(doc);
++ QVERIFY(error.error != QJsonParseError::NoError);
++ ++messages;
++ });
++
++ QCOMPARE(pipe.end1()->write(header), header.size());
++ QTRY_COMPARE(errors, 1);
++ QTRY_COMPARE(messages, 1);
++ }
++
++ void tst_QLanguageServer::lifecycle()
++ {
++ QIOPipe pipe;
++ pipe.open(QIODevice::ReadWrite);
++
++ QLanguageServerProtocol server([&pipe](const QByteArray &data) {
++ QMetaObject::invokeMethod(pipe.end1(), [&pipe, data]() { pipe.end1()->write(data); });
++ });
++ TestRigEventHandler handler(&server, pipe.end1());
++
++ QLanguageServerJsonRpcTransport transport;
++ setTransportDevice(&transport, pipe.end2());
++
++ QJsonRpcProtocol protocol;
++ protocol.setTransport(&transport);
++
++ int id = 0;
++ sendAndWaitJsonRpc(&protocol, {
++ ++id,
++ "initialize",
++ QJsonObject({
++ { "processId", QJsonValue::Null },
++ { "rootUri", QJsonValue::Null },
++ { "capabilities", QJsonObject() }
++ })
++ }, [&](const QJsonRpcProtocol::Response &response) {
++ QCOMPARE(response.id, QJsonValue(id));
++ QVERIFY2(response.errorMessage.isEmpty(), qPrintable(response.errorMessage));
++ QVERIFY(!response.errorCode.isDouble());
++ QCOMPARE(QJsonDocument(response.data.toObject()).toJson(QJsonDocument::Compact),
++ "{\"capabilities\":{}}");
++ });
++
++ protocol.sendNotification({
++ "initialized",
++ QJsonObject()
++ });
++
++ sendAndWaitJsonRpc(&protocol, {
++ ++id,
++ "shutdown",
++ QJsonValue::Undefined
++ }, [&](const QJsonRpcProtocol::Response &response) {
++ QCOMPARE(response.id, QJsonValue(id));
++ QVERIFY2(response.errorMessage.isEmpty(), qPrintable(response.errorMessage));
++ QVERIFY(!response.errorCode.isDouble());
++ QVERIFY(response.data.isNull());
++ });
++
++ QVERIFY(!handler.hasExited());
++
++ protocol.sendNotification({
++ "exit",
++ QJsonValue::Undefined
++ });
++
++ QTRY_VERIFY(handler.hasExited());
++ }
++
++ void tst_QLanguageServer::logOrShowMessage(const QString &method)
++ {
++ TestRig test;
++ test.open();
++
++ const QString message = QStringLiteral("msmsmsmsmsg");
++ bool messageReceived = false;
++ test.client.setMessageHandler(
++ method,
++ new NotificationHandler([&](const QJsonRpcProtocol::Notification ¬ification) {
++ const QJsonObject params = notification.params.toObject();
++ QCOMPARE(params["type"].toInt(), 4); // MessageType::Log
++ QCOMPARE(params["message"].toString(), message);
++ messageReceived = true;
++ }));
++
++ if (method == "window/showMessage") {
++ QLspSpecification::ShowMessageParams params;
++ params.type = QLspSpecification::MessageType::Log;
++ params.message = message.toUtf8();
++ test.protocol.notifyShowMessage(params);
++ } else if (method == "window/logMessage") {
++ QLspSpecification::LogMessageParams params;
++ params.type = QLspSpecification::MessageType::Log;
++ params.message = message.toUtf8();
++ test.protocol.notifyLogMessage(params);
++ } else {
++ QFAIL("wrong method");
++ }
++
++ QTRY_VERIFY(messageReceived);
++ }
++
++ void tst_QLanguageServer::windowShowMessage()
++ {
++ logOrShowMessage("window/showMessage");
++ }
++
++ void tst_QLanguageServer::windowShowMessageRequest()
++ {
++ TestRig test;
++ test.open();
++
++ const QString message = QStringLiteral("msmsmsmsmsg");
++ auto item = [](const char *title) {
++ QLspSpecification::MessageActionItem item;
++ item.title = title;
++ return item;
++ };
++
++ const QList<QLspSpecification::MessageActionItem> actions({
++ item("aaa"), item("bbb"), item("ccc")
++ });
++
++ bool messageReceived = false;
++ test.client.setMessageHandler(
++ "window/showMessageRequest",
++ new RequestHandler([&](const QJsonRpcProtocol::Request &request) {
++ const QJsonObject params = request.params.toObject();
++ [&]() {
++ QCOMPARE(params["type"].toInt(), 4); // MessageType::Log
++ QCOMPARE(params["message"].toString(), message);
++ QCOMPARE(params["actions"].toArray(), QJsonArray({
++ QJsonObject{{"title", "aaa"}},
++ QJsonObject{{"title", "bbb"}},
++ QJsonObject{{"title", "ccc"}},
++ }));
++ }();
++ messageReceived = true;
++ const QJsonRpcProtocol::Response response = {
++ request.id,
++ QJsonObject{{"title", "aaa"}},
++ QJsonValue::Undefined,
++ QString()
++ };
++ return response;
++ }));
++
++ QLspSpecification::ShowMessageRequestParams params;
++ params.type = QLspSpecification::MessageType::Log;
++ params.message = message.toUtf8();
++ params.actions = actions;
++
++ test.protocol.requestShowMessageRequest(
++ params,
++ [&](const std::variant<std::nullptr_t, QLspSpecification::MessageActionItem> &_r) {
++ auto r = std::get_if<QLspSpecification::MessageActionItem>(&_r);
++ QVERIFY(r);
++ QCOMPARE(r->title, "aaa");
++ });
++
++ QTRY_VERIFY(messageReceived);
++ }
++
++ void tst_QLanguageServer::windowLogMessage()
++ {
++ logOrShowMessage("window/logMessage");
++ }
++
++ void tst_QLanguageServer::telemetryEvent()
++ {
++ TestRig test;
++ test.open();
++
++ const QJsonObject message { { QStringLiteral("a"), 42 } };
++ bool messageReceived = false;
++ test.client.setMessageHandler(
++ "telemetry/event",
++ new NotificationHandler([&](const QJsonRpcProtocol::Notification ¬ification) {
++ QCOMPARE(notification.params, message);
++ messageReceived = true;
++ }));
++
++ test.protocol.notifyTelemetryEvent(message);
++ QTRY_VERIFY(messageReceived);
++ }
++
++ static void checkRequestInvalid(TestRig *test, const QLspSpecification::RegistrationParams ¶ms)
++ {
++ test->protocol.requestRegistration(params, []() {
++ QFAIL("server responded to invalid request");
++ });
++ }
++
++ static void checkRequestValid(TestRig *test, const QLspSpecification::RegistrationParams ¶ms)
++ {
++ test->protocol.requestRegistration(
++ params, [](){}, [](const QLspSpecification::ResponseError &) {
++ QFAIL("server failed on valid request");
++ });
++ }
++
++ static void checkRequestInvalid(
++ TestRig *test, const QLspSpecification::UnregistrationParams ¶ms)
++ {
++ test->protocol.requestUnregistration(params, []() {
++ QFAIL("server responded to invalid request");
++ });
++ }
++
++ static void checkRequestValid(TestRig *test, const QLspSpecification::UnregistrationParams ¶ms)
++ {
++ test->protocol.requestUnregistration(
++ params, [](){}, [](const QLspSpecification::ResponseError &) {
++ QFAIL("server failed on valid request");
++ });
++ }
++
++ void tst_QLanguageServer::clientRegisterCapability()
++ {
++ TestRig test;
++ QLspSpecification::ServerCapabilities serverCapabilities;
++ serverCapabilities.referencesProvider = true;
++ test.open(serverCapabilities);
++
++ QLspSpecification::RegistrationParams params;
++ QLspSpecification::Registration referencesRegistration;
++ referencesRegistration.method = "textDocument/references";
++ params.registrations = { referencesRegistration };
++
++ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"ERROR -32601"_s));
++ checkRequestInvalid(&test, params);
++
++ QLspSpecification::ReferenceClientCapabilities references;
++ references.dynamicRegistration = true;
++ QLspSpecification::TextDocumentClientCapabilities textDocumentCapabilities;
++ textDocumentCapabilities.references = references;
++ QLspSpecification::ClientCapabilities clientCapabilities;
++ clientCapabilities.textDocument = textDocumentCapabilities;
++
++ test.initialize(clientCapabilities);
++
++ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"ERROR -32601"_s));
++ checkRequestInvalid(&test, params);
++
++ auto handler = [&](const QJsonRpcProtocol::Request &request) {
++ [&]() {
++ const QJsonObject params = request.params.toObject();
++ QCOMPARE(params.length(), 1);
++ const QJsonArray registrations = params.begin().value().toArray();
++ QCOMPARE(registrations.count(), 1);
++ const QJsonObject registration = registrations[0].toObject();
++ QCOMPARE(registration["method"].toString(), QStringLiteral("textDocument/references"));
++ }();
++
++ const QJsonRpcProtocol::Response response = {
++ request.id,
++ QJsonValue::Null,
++ QJsonValue::Undefined,
++ QString()
++ };
++ return response;
++ };
++
++ test.client.setMessageHandler("client/registerCapability", new RequestHandler(handler));
++
++ checkRequestValid(&test, params);
++
++ const QLspSpecification::Registration registration = params.registrations.first();
++ QLspSpecification::UnregistrationParams unregisterParams;
++ unregisterParams.unregisterations = {
++ QLspSpecification::Unregistration { registration.id, registration.method }
++ };
++
++ checkRequestInvalid(&test, unregisterParams);
++
++ test.client.setMessageHandler("client/unregisterCapability", new RequestHandler(handler));
++
++ checkRequestValid(&test, unregisterParams);
++ }
++
++ void tst_QLanguageServer::setRequestHandler()
++ {
++ TestRig test;
++
++ const QList<QLspSpecification::Location> locations = {
++ QLspSpecification::Location { "a.b.x", QLspSpecification::Range() }
++ };
++
++ test.protocol.registerReferenceRequestHandler(
++ [locations](const QByteArray &, const QLspSpecification::ReferenceParams &,
++ QLspSpecification::LSPPartialResponse<std::variant<
++ QList<QLspSpecification::Location>, std::nullptr_t>,
++ QList<QLspSpecification::Location>> &&result) {
++ result.sendResponse(locations);
++ });
++
++ test.open();
++
++ QJsonRpcProtocol::Response response;
++ auto requestReferences = [&]() {
++ sendAndWaitJsonRpc(&test.client, {
++ 4,
++ "textDocument/references",
++ QJsonValue()
++ }, [&](const QJsonRpcProtocol::Response &received) {
++ response = received;
++ });
++ };
++
++ requestReferences();
++ QCOMPARE(response.errorCode.toInt(), -32002);
++
++ test.initialize();
++ QVERIFY(test.handler->isInitialized());
++ requestReferences();
++ QVERIFY(response.errorCode.isUndefined());
++ QCOMPARE(response.data, QTypedJson::toJsonValue(locations));
++ }
++
++ void tst_QLanguageServer::rejectOutOfBoundMessages_data()
++ {
++ QTest::addColumn<QByteArray>("content");
++ QTest::addColumn<std::optional<int>>("maxContentLength");
++ QTest::addColumn<QString>("expectedWarningMessage");
++
++ const auto quint64_max = std::numeric_limits<quint64>::max();
++ QTest::newRow("Exceeding INT_MAX Limit")
++ << QString("Content-Length: %1\r\n\r\n{...}").arg(quint64_max).toUtf8()
++ << std::optional<int>()
++ << u"Invalid Content-Length: %1"_s.arg(quint64_max);
++ QTest::newRow("Valid message within limit")
++ << QByteArray("Content-Length: 500\r\n\r\n{...}")
++ << std::optional<int>(1000) << QString();
++ QTest::newRow("Invalid Message Exceeding Configured Limit")
++ << QByteArray("Content-Length: 200\r\n\r\n")
++ << std::optional<int>(100)
++ << u"Content-Length 200 exceeds maximum allowed size 100"_s;
++ QTest::newRow("Configuring Negative Limit Clamped to Zero")
++ << QByteArray("Content-Length: 10\r\n\r\n") << std::optional<int>(-100)
++ << u"Content-Length 10 exceeds maximum allowed size 0"_s;;
++ QTest::newRow("Negative Content-Length")
++ << QByteArray("Content-Length: -100\r\n\r\n")
++ << std::optional<int>()
++ << u"Negative Content-Length: -100"_s;
++ QTest::newRow("Zero Content-Length")
++ << QByteArray("Content-Length: 0\r\n\r\n")
++ << std::optional<int>() << QString();
++ }
++
++ void tst_QLanguageServer::rejectOutOfBoundMessages()
++ {
++ QFETCH(QByteArray, content);
++ QFETCH(std::optional<int>, maxContentLength);
++ QFETCH(QString, expectedWarningMessage);
++
++ Q_UNUSED(maxContentLength);
++ QLanguageServerJsonRpcTransport transport;
++ if (maxContentLength.has_value())
++ transport.setMaxContentLength(*maxContentLength);
++
++ int warnings = 0;
++ QSet<QString> warningMessages;
++ transport.setDiagnosticHandler(
++ [&](QJsonRpcTransport::DiagnosticLevel level, const QString &msg) {
++ if (level == QJsonRpcTransport::Warning)
++ ++warnings;
++ warningMessages.insert(msg);
++ });
++ transport.receiveData(content);
++ if (expectedWarningMessage.isEmpty()) {
++ QCOMPARE(warnings, 0);
++ return;
++ }
++ QVERIFY(warningMessages.contains(expectedWarningMessage));
++ }
++
++ QTEST_MAIN(tst_QLanguageServer)
++
++ #include <tst_qlanguageserver.moc>