mirror of
https://github.com/qt/qtmqtt.git
synced 2026-10-25 16:16:26 +00:00
- Every MQTT packet begins with a fixed header, whose second field is the Remaining Length: the number of bytes that follow. It is not a byte. It is spread over one to four bytes, seven value bits each, with the top bit meaning "another byte follows". So values up to 127 take one byte, and larger values take more. Six packet types read it as a single quint8: SUBACK, UNSUBACK, PUBACK, PUBREC, PUBREL and PUBCOMP. For most of them that used to be correct. In MQTT 3.1.1 those packets contain nothing but a two byte identifier, so the length was always exactly 2, which always fits in one byte. The code did not even use the byte it read: it checked that the value was 2 and then hardcoded the length. MQTT 5.0 support (ac66658) gave those packets an optional reason code and properties, so the length can now be far above 127. That commit switched off the "must be 2" check for MQTT 5.0 and began using the byte as the real length, but kept reading only one byte. The check had been the only thing making a one byte read safe, because a multi-byte length always has its top bit set and so could never equal 2. SUBACK never had a fixed length at all, since its payload carries one reason code per subscribed topic filter. It has been misparsed for any SUBACK above 127 bytes since the parser was written (d96011b). Reading one byte of a two byte length consumes one byte too few, so the remainder of the packet is then read as the next packet's header. The client rejects a valid acknowledgement and stays out of step for the rest of the connection. Brokers trigger this on ordinary traffic as soon as they attach a reason string or user properties. All six now use readVariableByteInteger(), and each rejects a length too short to hold the fields the specification requires. PUBREL also accepted a length of exactly 2 even for MQTT 5.0, which rejected valid PUBREL packets carrying a reason code. It now accepts 2 or more there, and still requires exactly 2 for MQTT 3.1.1. - Adds three regression tests driving the parser through the existing FakeServer, so they need no broker. Pick-to: 6.11 6.12 6.8 Task-number: QTBUG-149569 Change-Id: Ib239d836556e0cc14e13c116dee757fdd1a7ae81 Reviewed-by: Ivan Solovev <ivan.solovev@qt.io>
1209 lines
42 KiB
C++
1209 lines
42 KiB
C++
// Copyright (C) 2017 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
#include "broker_connection.h"
|
|
|
|
#include <QtCore/QString>
|
|
#include <QtNetwork/QTcpServer>
|
|
#include <QtTest/QtTest>
|
|
#include <QtTest/QSignalSpy>
|
|
#include <QtMqtt/QMqttClient>
|
|
|
|
#include <limits>
|
|
|
|
class Tst_QMqttClient : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
Tst_QMqttClient();
|
|
|
|
private Q_SLOTS:
|
|
void initTestCase();
|
|
void cleanupTestCase();
|
|
void getSetCheck_data();
|
|
void getSetCheck();
|
|
void sendReceive_data();
|
|
void sendReceive();
|
|
void retainMessage_data();
|
|
void retainMessage();
|
|
void willMessage_data();
|
|
void willMessage();
|
|
void compliantTopic_data();
|
|
void compliantTopic();
|
|
void subscribeLongTopic_data();
|
|
void subscribeLongTopic();
|
|
void dataIncludingZero_data();
|
|
void dataIncludingZero();
|
|
void publishLongTopic_data();
|
|
void publishLongTopic();
|
|
void reconnect_QTBUG65726_data();
|
|
void reconnect_QTBUG65726();
|
|
void openIODevice_QTBUG66955_data();
|
|
void openIODevice_QTBUG66955();
|
|
void subackLongRemainingLength();
|
|
void pubackLongRemainingLength();
|
|
void pubrelWithReasonCode();
|
|
void shortRemainingLength_data();
|
|
void shortRemainingLength();
|
|
void staticProperties_QTBUG_67176_data();
|
|
void staticProperties_QTBUG_67176();
|
|
void authentication();
|
|
void messageStatus_data();
|
|
void messageStatus();
|
|
void messageStatusReceive_data();
|
|
void messageStatusReceive();
|
|
void subscriptionIdsOverlap();
|
|
void keepAlive_data();
|
|
void keepAlive();
|
|
void reconnect();
|
|
private:
|
|
QProcess m_brokerProcess;
|
|
QString m_testBroker;
|
|
quint16 m_port{0};
|
|
};
|
|
|
|
Tst_QMqttClient::Tst_QMqttClient()
|
|
{
|
|
}
|
|
|
|
void Tst_QMqttClient::initTestCase()
|
|
{
|
|
m_testBroker = invokeOrInitializeBroker(&m_brokerProcess);
|
|
if (m_testBroker.isEmpty())
|
|
qFatal("No MQTT broker present to test against.");
|
|
}
|
|
|
|
void Tst_QMqttClient::cleanupTestCase()
|
|
{
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::getSetCheck_data)
|
|
|
|
void Tst_QMqttClient::getSetCheck()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
VersionClient(mqttVersion, client);
|
|
|
|
QVERIFY(client.clientId().size() > 0);
|
|
const QString clientId = QLatin1String("testclient123");
|
|
client.setClientId(clientId);
|
|
QCOMPARE(client.clientId(), clientId);
|
|
|
|
QCOMPARE(client.hostname(), QString());
|
|
const QString hostname = QLatin1String("qt.io");
|
|
client.setHostname(hostname);
|
|
QCOMPARE(client.hostname(), hostname);
|
|
|
|
QCOMPARE(client.port(), quint16(0));
|
|
client.setPort(1883);
|
|
QCOMPARE(client.port(), quint16(1883));
|
|
|
|
QCOMPARE(client.keepAlive(), quint16(60));
|
|
client.setKeepAlive(10);
|
|
QCOMPARE(client.keepAlive(), quint16(10));
|
|
|
|
// Available protocol versions
|
|
QMqttClient client2;
|
|
QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1);
|
|
client2.setProtocolVersion(QMqttClient::ProtocolVersion(0));
|
|
QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1);
|
|
client2.setProtocolVersion(QMqttClient::ProtocolVersion(6));
|
|
QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1_1);
|
|
client2.setProtocolVersion(QMqttClient::MQTT_3_1);
|
|
QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_3_1);
|
|
client2.setProtocolVersion(QMqttClient::MQTT_5_0);
|
|
QCOMPARE(client2.protocolVersion(), QMqttClient::MQTT_5_0);
|
|
|
|
#ifdef QT_BUILD_INTERNAL
|
|
if (qEnvironmentVariableIsSet("QT_MQTT_TEST_USERNAME"))
|
|
QEXPECT_FAIL("", "Default username has been overwritten.", Continue);
|
|
#endif
|
|
QCOMPARE(client.username(), QString());
|
|
#ifdef QT_BUILD_INTERNAL
|
|
if (qEnvironmentVariableIsSet("QT_MQTT_TEST_PASSWORD"))
|
|
QEXPECT_FAIL("", "Default username has been overwritten.", Continue);
|
|
#endif
|
|
QCOMPARE(client.password(), QString());
|
|
QCOMPARE(client.cleanSession(), true);
|
|
QCOMPARE(client.willTopic(), QString());
|
|
QCOMPARE(client.willMessage(), QByteArray());
|
|
QCOMPARE(client.willQoS(), quint8(0));
|
|
QCOMPARE(client.willRetain(), false);
|
|
QCOMPARE(client.autoKeepAlive(), true);
|
|
client.setAutoKeepAlive(false);
|
|
QCOMPARE(client.autoKeepAlive(), false);
|
|
}
|
|
|
|
void Tst_QMqttClient::sendReceive_data()
|
|
{
|
|
QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion");
|
|
QTest::addColumn<QByteArray>("data");
|
|
|
|
QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0};
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":empty")) << versions[i] << QByteArray();
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":simple")) << versions[i] << QByteArray("This is a test message");
|
|
QByteArray d;
|
|
d.fill('A', 500);
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":big")) << versions[i] << d;
|
|
d.fill('B', (128 * 128 * 128) + 4);
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":huge")) << versions[i] << d;
|
|
}
|
|
}
|
|
|
|
void Tst_QMqttClient::sendReceive()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
QFETCH(QByteArray, data);
|
|
const QString testTopic = QLatin1String("Qt/QMqttClient/Topic");
|
|
|
|
VersionClient(mqttVersion, publisher);
|
|
publisher.setClientId(QLatin1String("publisher"));
|
|
publisher.setHostname(m_testBroker);
|
|
publisher.setPort(m_port);
|
|
|
|
publisher.connectToHost();
|
|
QTRY_COMPARE(publisher.state(), QMqttClient::Connected);
|
|
|
|
VersionClient(mqttVersion, subscriber);
|
|
subscriber.setClientId(QLatin1String("subscriber"));
|
|
subscriber.setHostname(m_testBroker);
|
|
subscriber.setPort(m_port);
|
|
|
|
subscriber.connectToHost();
|
|
QTRY_COMPARE(subscriber.state(), QMqttClient::Connected);
|
|
|
|
bool received = false;
|
|
bool verified = false;
|
|
auto sub = subscriber.subscribe(testTopic, 1);
|
|
QVERIFY(sub);
|
|
connect(sub, &QMqttSubscription::messageReceived, sub, [&](QMqttMessage msg) {
|
|
verified = msg.payload() == data;
|
|
received = true;
|
|
});
|
|
|
|
QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed);
|
|
|
|
if (subscriber.protocolVersion() == QMqttClient::MQTT_5_0 &&
|
|
(quint32)data.size() > subscriber.serverConnectionProperties().maximumPacketSize())
|
|
QSKIP("The MQTT 5 test broker does not support huge packages.", SkipOnce);
|
|
publisher.publish(testTopic, data, 1);
|
|
|
|
QTRY_VERIFY2_WITH_TIMEOUT(received, "Subscriber did not receive message", std::chrono::seconds(180));
|
|
QVERIFY2(verified, "Subscriber received different message");
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::retainMessage_data)
|
|
|
|
void Tst_QMqttClient::retainMessage()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
const QString testTopic = QLatin1String("Qt/QMqttClient/Topic2");
|
|
const QByteArray testMessage("retainedMessage");
|
|
|
|
// Publisher
|
|
VersionClient(mqttVersion, publisher);
|
|
publisher.setClientId(QLatin1String("publisher"));
|
|
publisher.setHostname(m_testBroker);
|
|
publisher.setPort(m_port);
|
|
|
|
publisher.connectToHost();
|
|
QTRY_COMPARE(publisher.state(), QMqttClient::Connected);
|
|
|
|
// [MQTT-3.3.1-7]
|
|
// If the Server receives a QoS 0 message with the RETAIN flag set to 1 it MUST
|
|
// discard any message previously retained for that topic. It SHOULD store the
|
|
// new QoS 0 message
|
|
publisher.publish(testTopic, QByteArray(), 0, true);
|
|
// We cannot wait as 0 QoS does not send confirmation
|
|
QTest::qWait(500);
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
int msgCount = 0;
|
|
|
|
QSignalSpy publishSpy(&publisher, SIGNAL(messageSent(qint32)));
|
|
publisher.publish(testTopic, testMessage, 1, i == 1 ? true : false);
|
|
QTRY_COMPARE(publishSpy.size(), 1);
|
|
|
|
VersionClient(mqttVersion, sub);
|
|
sub.setClientId(QLatin1String("SubA"));
|
|
sub.setHostname(m_testBroker);
|
|
sub.setPort(m_port);
|
|
connect(&sub, &QMqttClient::messageReceived, &sub,
|
|
[&msgCount, testMessage](const QByteArray &msg) {
|
|
if (msg == testMessage)
|
|
msgCount++;
|
|
});
|
|
|
|
QSignalSpy messageSpy(&sub, SIGNAL(messageReceived(QByteArray,QMqttTopicName)));
|
|
sub.connectToHost();
|
|
QTRY_COMPARE(sub.state(), QMqttClient::Connected);
|
|
|
|
auto subscription = sub.subscribe(testTopic);
|
|
QTRY_COMPARE(subscription->state(), QMqttSubscription::Subscribed);
|
|
|
|
QTRY_VERIFY(msgCount == i);
|
|
}
|
|
publisher.disconnect();
|
|
}
|
|
|
|
void Tst_QMqttClient::willMessage_data()
|
|
{
|
|
QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion");
|
|
QTest::addColumn<QByteArray>("willMessage");
|
|
|
|
QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0};
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":simple")) << versions[i] << QByteArray("The client connection is gone.");
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":empty")) << versions[i] << QByteArray();
|
|
}
|
|
}
|
|
|
|
void Tst_QMqttClient::willMessage()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
QFETCH(QByteArray, willMessage);
|
|
|
|
const QString willTopic = QLatin1String("Qt/QMqttClient/will/topic");
|
|
|
|
// Client A connects
|
|
VersionClient(mqttVersion, client1);
|
|
client1.setHostname(m_testBroker);
|
|
client1.setPort(m_port);
|
|
client1.connectToHost();
|
|
QTRY_COMPARE(client1.state(), QMqttClient::Connected);
|
|
|
|
if (!client1.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
|
|
auto client1Sub = client1.subscribe(willTopic, 1);
|
|
connect(client1Sub, &QMqttSubscription::messageReceived,
|
|
client1Sub, [=](QMqttMessage message) {
|
|
Q_UNUSED(message);
|
|
// Just debug purposes
|
|
//qDebug() << "Got something:" << message;
|
|
});
|
|
QTRY_COMPARE(client1Sub->state(), QMqttSubscription::Subscribed);
|
|
|
|
QSignalSpy messageSpy(client1Sub, SIGNAL(messageReceived(QMqttMessage)));
|
|
|
|
// Client B connects (via TcpSocket)
|
|
QTcpSocket sock;
|
|
sock.connectToHost(client1.hostname(), client1.port());
|
|
QVERIFY(sock.waitForConnected());
|
|
|
|
for (int i = 1; i > 0; --i) {
|
|
VersionClient(mqttVersion, willClient);
|
|
if (i == 1)
|
|
willClient.setTransport(&sock, QMqttClient::AbstractSocket);
|
|
else {
|
|
willClient.setHostname(m_testBroker);
|
|
willClient.setPort(m_port);
|
|
}
|
|
willClient.setWillQoS(1);
|
|
willClient.setWillTopic(willTopic);
|
|
willClient.setWillMessage(willMessage);
|
|
willClient.connectToHost();
|
|
QTRY_COMPARE(willClient.state(), QMqttClient::Connected);
|
|
|
|
willClient.publish(QLatin1String("noninteresting"), "just something");
|
|
|
|
// Be evil and kill the connection without DISCONNECT
|
|
// Should send will message to client1.
|
|
// When you manually disconnect (send the DISCONNECT command) no will message
|
|
// is sent
|
|
if (i == 1)
|
|
sock.disconnectFromHost();
|
|
else
|
|
willClient.disconnectFromHost();
|
|
QTest::qWait(500);
|
|
QTRY_COMPARE(messageSpy.size(), i);
|
|
}
|
|
}
|
|
|
|
void Tst_QMqttClient::compliantTopic_data()
|
|
{
|
|
QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion");
|
|
QTest::addColumn<QString>("topic");
|
|
|
|
QList<QMqttClient::ProtocolVersion> versions{QMqttClient::MQTT_3_1_1, QMqttClient::MQTT_5_0};
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":simple")) << versions[i] << QString::fromLatin1("Qt/QMqttClient/topic");
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":subPath")) << versions[i] << QString::fromLatin1("Qt/QMqttClient/topic/subtopic");
|
|
|
|
if (versions[i] != QMqttClient::MQTT_5_0) {
|
|
QString l;
|
|
l.fill(QLatin1Char('T'), std::numeric_limits<std::uint16_t>::max());
|
|
QTest::newRow(qPrintable(QString::number(versions[i]) + ":maxSize")) << versions[i] << l;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Tst_QMqttClient::compliantTopic()
|
|
{
|
|
QFETCH(QString, topic);
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
QString truncTopic = topic;
|
|
truncTopic.truncate(std::numeric_limits<std::uint16_t>::max());
|
|
|
|
VersionClient(mqttVersion, publisher);
|
|
publisher.setClientId(QLatin1String("publisher"));
|
|
publisher.setHostname(m_testBroker);
|
|
publisher.setPort(m_port);
|
|
|
|
publisher.connectToHost();
|
|
QTRY_COMPARE(publisher.state(), QMqttClient::Connected);
|
|
|
|
VersionClient(mqttVersion, subscriber);
|
|
subscriber.setClientId(QLatin1String("subscriber"));
|
|
subscriber.setHostname(m_testBroker);
|
|
subscriber.setPort(m_port);
|
|
|
|
subscriber.connectToHost();
|
|
QTRY_COMPARE(subscriber.state(), QMqttClient::Connected);
|
|
|
|
bool received = false;
|
|
bool verified = false;
|
|
connect(&subscriber, &QMqttClient::messageReceived,
|
|
&subscriber, [&](const QByteArray &, const QMqttTopicName &t) {
|
|
received = true;
|
|
verified = t == truncTopic;
|
|
});
|
|
|
|
auto client1Sub = subscriber.subscribe(truncTopic, 1);
|
|
QTRY_COMPARE(client1Sub->state(), QMqttSubscription::Subscribed);
|
|
|
|
publisher.publish(topic, QByteArray("Msgs"), 1);
|
|
|
|
QTRY_VERIFY2(received, "Subscriber did not receive message");
|
|
QVERIFY2(verified, "Subscriber received different message");
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::subscribeLongTopic_data)
|
|
|
|
void Tst_QMqttClient::subscribeLongTopic()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
VersionClient(mqttVersion, subscriber);
|
|
subscriber.setClientId(QLatin1String("subscriber"));
|
|
subscriber.setHostname(m_testBroker);
|
|
subscriber.setPort(m_port);
|
|
|
|
subscriber.connectToHost();
|
|
QTRY_COMPARE(subscriber.state(), QMqttClient::Connected);
|
|
|
|
QString topic;
|
|
topic.fill(QLatin1Char('s'), 2 * std::numeric_limits<std::uint16_t>::max());
|
|
auto sub = subscriber.subscribe(topic);
|
|
QCOMPARE(sub, nullptr);
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::dataIncludingZero_data)
|
|
|
|
void Tst_QMqttClient::dataIncludingZero()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
QByteArray data;
|
|
const int dataSize = 200;
|
|
data.fill('A', dataSize);
|
|
data[100] = '\0';
|
|
|
|
VersionClient(mqttVersion, client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
bool received = false;
|
|
bool verified = false;
|
|
bool correctSize = false;
|
|
const QString testTopic(QLatin1String("Qt/QMqttClient/some/topic"));
|
|
auto sub = client.subscribe(testTopic, 1);
|
|
QVERIFY(sub);
|
|
connect(sub, &QMqttSubscription::messageReceived,
|
|
sub, [&](QMqttMessage msg) {
|
|
verified = msg.payload() == data;
|
|
correctSize = msg.payload().size() == dataSize;
|
|
received = true;
|
|
});
|
|
|
|
QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed);
|
|
|
|
client.publish(testTopic, data, 1);
|
|
|
|
QTRY_VERIFY2(received, "Subscriber did not receive message");
|
|
QVERIFY2(verified, "Subscriber received different message");
|
|
QVERIFY2(correctSize, "Subscriber received message of different size");
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::publishLongTopic_data)
|
|
|
|
void Tst_QMqttClient::publishLongTopic()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
VersionClient(mqttVersion, publisher);
|
|
publisher.setClientId(QLatin1String("publisher"));
|
|
publisher.setHostname(m_testBroker);
|
|
publisher.setPort(m_port);
|
|
|
|
publisher.connectToHost();
|
|
QTRY_COMPARE(publisher.state(), QMqttClient::Connected);
|
|
|
|
QString topic;
|
|
topic.fill(QLatin1Char('s'), 2 * std::numeric_limits<std::uint16_t>::max());
|
|
auto pub = publisher.publish(topic);
|
|
QCOMPARE(pub, -1);
|
|
}
|
|
|
|
class FakeServer : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
FakeServer() {
|
|
server = new QTcpServer(this);
|
|
connect(server, &QTcpServer::newConnection, this, &FakeServer::createSocket);
|
|
server->listen(QHostAddress::Any, 5726);
|
|
}
|
|
// Sends raw bytes to the client, so a test can drive the parser with an exact
|
|
// byte sequence.
|
|
void send(const QByteArray &data) { socket->write(data); }
|
|
public slots:
|
|
void createSocket() {
|
|
socket = server->nextPendingConnection();
|
|
connectionAcknowledged = false;
|
|
lastRequest.clear();
|
|
connect(socket, &QTcpSocket::readyRead, this, &FakeServer::connectionRequested);
|
|
}
|
|
|
|
void connectionRequested() {
|
|
// We assume the first packet is always a connect statement, so no verification
|
|
// is done. Everything after that is recorded for the test to answer itself.
|
|
if (connectionAcknowledged) {
|
|
lastRequest.append(socket->readAll());
|
|
return;
|
|
}
|
|
connectionAcknowledged = true;
|
|
socket->readAll();
|
|
QByteArray response;
|
|
response += 0x20;
|
|
response += quint8(2); // Payload size
|
|
if (!connectionSuccess) {
|
|
response += quint8(255); // Causes ProtocolViolation
|
|
response += quint8(13);
|
|
} else {
|
|
response += char(0); // ackFlags
|
|
response += char(0); // result
|
|
if (version == QMqttClient::MQTT_5_0) {
|
|
response += char(0); // No properties
|
|
response[1] = response[1] + 1; // Adjust payload size
|
|
}
|
|
}
|
|
qDebug() << "Fake server response:" << connectionSuccess;
|
|
socket->write(response);
|
|
}
|
|
public:
|
|
QTcpServer *server;
|
|
QTcpSocket *socket;
|
|
QMqttClient::ProtocolVersion version{QMqttClient::MQTT_3_1_1};
|
|
bool connectionSuccess{false};
|
|
bool connectionAcknowledged{false};
|
|
QByteArray lastRequest;
|
|
};
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::reconnect_QTBUG65726_data)
|
|
|
|
void Tst_QMqttClient::reconnect_QTBUG65726()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
FakeServer server;
|
|
|
|
VersionClient(mqttVersion, client);
|
|
client.setClientId(QLatin1String("bugclient"));
|
|
client.setHostname(QLatin1String("localhost"));
|
|
client.setPort(5726);
|
|
|
|
if (!client.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
|
|
server.version = client.protocolVersion();
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
QTRY_COMPARE(client.error(), QMqttClient::ProtocolViolation);
|
|
|
|
server.connectionSuccess = true;
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
QTRY_COMPARE(client.error(), QMqttClient::NoError);
|
|
}
|
|
|
|
class IOTransport : public QIODevice
|
|
{
|
|
public:
|
|
bool open(OpenMode mode) override {
|
|
return QIODevice::open(mode);
|
|
}
|
|
qint64 readData(char *data, qint64 maxlen) override {
|
|
Q_UNUSED(data);
|
|
Q_UNUSED(maxlen);
|
|
return 0;
|
|
}
|
|
qint64 writeData(const char *data, qint64 len) override {
|
|
Q_UNUSED(data);
|
|
Q_UNUSED(len);
|
|
if (data[0] == 0x10)
|
|
written = 1;
|
|
else
|
|
qDebug() << "Received unknown/invalid data";
|
|
return len;
|
|
}
|
|
QAtomicInt written{0};
|
|
};
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::openIODevice_QTBUG66955_data)
|
|
|
|
void Tst_QMqttClient::openIODevice_QTBUG66955()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
IOTransport trans;
|
|
trans.open(QIODevice::ReadWrite);
|
|
|
|
VersionClient(mqttVersion, client);
|
|
client.setTransport(&trans, QMqttClient::IODevice);
|
|
client.connectToHost();
|
|
|
|
QTRY_COMPARE(trans.written, 1);
|
|
}
|
|
|
|
// A reason string property (0x1f) of the requested content length.
|
|
static QByteArray reasonStringProperty(quint16 length)
|
|
{
|
|
QByteArray property;
|
|
property.append(char(0x1f));
|
|
property.append(char(length >> 8));
|
|
property.append(char(length & 0xff));
|
|
property.append(QByteArray(length, 'r'));
|
|
return property;
|
|
}
|
|
|
|
// The packet identifier of a packet the client wrote, skipping the packet type and
|
|
// its variable byte remaining length.
|
|
static quint16 packetIdentifier(const QByteArray &packet)
|
|
{
|
|
qsizetype pos = 1;
|
|
while (pos < packet.size() && (packet.at(pos) & 128) != 0)
|
|
++pos;
|
|
Q_ASSERT(pos < packet.size() - 2);
|
|
++pos;
|
|
return quint16(quint8(packet.at(pos)) << 8 | quint8(packet.at(pos + 1)));
|
|
}
|
|
|
|
static void appendPacketIdentifier(QByteArray *payload, quint16 id)
|
|
{
|
|
payload->append(char(id >> 8));
|
|
payload->append(char(id & 0xff));
|
|
}
|
|
|
|
// MQTT-2.2.3 (3.1.1) / MQTT-2.1.4 (5.0) The remaining length is a variable byte
|
|
// integer, so a packet longer than 127 bytes carries a two byte length whose first
|
|
// byte has the continuation bit set. Reading only that first byte consumes one byte
|
|
// too few and leaves the rest of the payload to be parsed as the next packet's
|
|
// fixed header.
|
|
void Tst_QMqttClient::subackLongRemainingLength()
|
|
{
|
|
FakeServer server;
|
|
server.version = QMqttClient::MQTT_5_0;
|
|
server.connectionSuccess = true;
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, client);
|
|
if (!client.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
client.setHostname(QLatin1String("localhost"));
|
|
client.setPort(5726);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
auto sub = client.subscribe(QMqttTopicFilter(QLatin1String("a/b")), 1);
|
|
QVERIFY(sub);
|
|
QTRY_VERIFY(server.lastRequest.size() >= 4); // The SUBSCRIBE arrived
|
|
|
|
// SUBACK with a reason string, which pushes the remaining length past 127.
|
|
QByteArray payload;
|
|
appendPacketIdentifier(&payload, packetIdentifier(server.lastRequest));
|
|
const QByteArray properties = reasonStringProperty(123);
|
|
payload.append(char(properties.size())); // Property length, 126
|
|
payload.append(properties);
|
|
payload.append(char(0x01)); // Reason code: maximum QoS 1
|
|
QCOMPARE(payload.size(), 130);
|
|
|
|
QByteArray packet;
|
|
packet.append(char(0x90));
|
|
packet.append(char(0x82)); // Remaining length 130, low seven bits plus continuation
|
|
packet.append(char(0x01)); // Remaining length 130, remaining bits
|
|
packet.append(payload);
|
|
server.send(packet);
|
|
|
|
QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed);
|
|
QCOMPARE(client.state(), QMqttClient::Connected);
|
|
QCOMPARE(client.error(), QMqttClient::NoError);
|
|
}
|
|
|
|
void Tst_QMqttClient::pubackLongRemainingLength()
|
|
{
|
|
FakeServer server;
|
|
server.version = QMqttClient::MQTT_5_0;
|
|
server.connectionSuccess = true;
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, client);
|
|
if (!client.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
client.setHostname(QLatin1String("localhost"));
|
|
client.setPort(5726);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
const qint32 id = client.publish(QMqttTopicName(QLatin1String("a/b")), QByteArray("x"), 1);
|
|
QVERIFY(id > 0);
|
|
QTRY_VERIFY(server.lastRequest.size() >= 4); // The PUBLISH arrived
|
|
|
|
QSignalSpy sentSpy(&client, &QMqttClient::messageSent);
|
|
|
|
// PUBACK with a reason string, which pushes the remaining length past 127.
|
|
QByteArray payload;
|
|
appendPacketIdentifier(&payload, quint16(id));
|
|
payload.append(char(0x00)); // Reason code: Success
|
|
const QByteArray properties = reasonStringProperty(123);
|
|
payload.append(char(properties.size())); // Property length, 126
|
|
payload.append(properties);
|
|
QCOMPARE(payload.size(), 130);
|
|
|
|
QByteArray packet;
|
|
packet.append(char(0x40));
|
|
packet.append(char(0x82)); // Remaining length 130, low seven bits plus continuation
|
|
packet.append(char(0x01)); // Remaining length 130, remaining bits
|
|
packet.append(payload);
|
|
server.send(packet);
|
|
|
|
QTRY_COMPARE(sentSpy.count(), 1);
|
|
QCOMPARE(sentSpy.at(0).at(0).toInt(), id);
|
|
QCOMPARE(client.state(), QMqttClient::Connected);
|
|
QCOMPARE(client.error(), QMqttClient::NoError);
|
|
}
|
|
|
|
// MQTT-3.6.2, MQTT-3.6.2.1 In MQTT 5.0 the PUBREL variable header holds a reason
|
|
// code and properties in addition to the packet identifier. A remaining length of 2
|
|
// only occurs when both are omitted, so a longer PUBREL must be accepted.
|
|
void Tst_QMqttClient::pubrelWithReasonCode()
|
|
{
|
|
FakeServer server;
|
|
server.version = QMqttClient::MQTT_5_0;
|
|
server.connectionSuccess = true;
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, client);
|
|
if (!client.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
client.setHostname(QLatin1String("localhost"));
|
|
client.setPort(5726);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
QMqtt::MessageStatus status = QMqtt::MessageStatus::Unknown;
|
|
QString reasonString;
|
|
connect(&client, &QMqttClient::messageStatusChanged, &client,
|
|
[&](qint32, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &properties)
|
|
{
|
|
status = s;
|
|
reasonString = properties.reason();
|
|
});
|
|
|
|
// PUBLISH, QoS 2, topic "a/b", packet identifier 7, no properties, payload "x".
|
|
// The client answers with a PUBREC, which puts it in the middle of the QoS 2
|
|
// exchange and makes it expect a PUBREL.
|
|
server.send(QByteArray::fromHex("34090003612f6200070078"));
|
|
QTRY_VERIFY(server.lastRequest.size() >= 4); // The PUBREC arrived
|
|
|
|
// PUBREL, packet identifier 7, reason code Success, and a reason string. Remaining
|
|
// length 12: 2 identifier + 1 reason code + 1 property length + 8 property bytes.
|
|
server.send(QByteArray::fromHex("620c000700081f000568656c6c6f"));
|
|
|
|
QTRY_COMPARE(status, QMqtt::MessageStatus::Released);
|
|
QCOMPARE(reasonString, QLatin1String("hello"));
|
|
QCOMPARE(client.state(), QMqttClient::Connected);
|
|
QCOMPARE(client.error(), QMqttClient::NoError);
|
|
}
|
|
|
|
// The remaining length counts the bytes that follow the fixed header, so a packet
|
|
// declaring less than the fields the specification requires is malformed. Accepting
|
|
// one leaves the parser reading past the end of the packet: the reason code loops in
|
|
// finalize_suback() and finalize_unsuback() read at least once regardless of what is
|
|
// left, and finalize_pubAckRecRelComp() always reads a packet identifier. Each of
|
|
// these packets must be rejected as a protocol violation instead.
|
|
void Tst_QMqttClient::shortRemainingLength_data()
|
|
{
|
|
QTest::addColumn<QMqttClient::ProtocolVersion>("mqttVersion");
|
|
QTest::addColumn<QByteArray>("packet");
|
|
|
|
// SUBACK, packet identifier 7, and nothing else. MQTT-3.9.3 requires at least one
|
|
// reason code, so the shortest legal remaining length is 3.
|
|
QTest::newRow("SUBACK 3.1.1, no reason code")
|
|
<< QMqttClient::MQTT_3_1_1 << QByteArray::fromHex("90020007");
|
|
// The same, plus an empty property block. MQTT-3.9.2.1 makes the property length
|
|
// mandatory in MQTT 5.0, so the shortest legal remaining length is 4.
|
|
QTest::newRow("SUBACK 5.0, no reason code")
|
|
<< QMqttClient::MQTT_5_0 << QByteArray::fromHex("9003000700");
|
|
// UNSUBACK, packet identifier 7, empty property block, no reason code. The
|
|
// property length is mandatory (MQTT-3.11.2.1) and at least one reason code is
|
|
// required (MQTT-3.11.3), so the shortest legal remaining length is 4.
|
|
QTest::newRow("UNSUBACK 5.0, no reason code")
|
|
<< QMqttClient::MQTT_5_0 << QByteArray::fromHex("b003000700");
|
|
// PUBACK with half a packet identifier. The reason code and the property length may
|
|
// both be omitted (MQTT-3.4.2.1), but the identifier may not, so the shortest legal
|
|
// remaining length is 2.
|
|
QTest::newRow("PUBACK 5.0, truncated packet identifier")
|
|
<< QMqttClient::MQTT_5_0 << QByteArray::fromHex("400100");
|
|
// The same for PUBREL, MQTT-3.6.2.1.
|
|
QTest::newRow("PUBREL 5.0, truncated packet identifier")
|
|
<< QMqttClient::MQTT_5_0 << QByteArray::fromHex("620100");
|
|
}
|
|
|
|
void Tst_QMqttClient::shortRemainingLength()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
QFETCH(QByteArray, packet);
|
|
|
|
FakeServer server;
|
|
server.version = mqttVersion;
|
|
server.connectionSuccess = true;
|
|
|
|
VersionClient(mqttVersion, client);
|
|
if (!client.isTcp())
|
|
QSKIP("This test is only for tcp");
|
|
client.setHostname(QLatin1String("localhost"));
|
|
client.setPort(5726);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
server.send(packet);
|
|
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
QCOMPARE(client.error(), QMqttClient::ProtocolViolation);
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::staticProperties_QTBUG_67176_data)
|
|
|
|
void Tst_QMqttClient::staticProperties_QTBUG_67176()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
|
|
VersionClient(mqttVersion, client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
|
|
auto clientVersion = client.protocolVersion();
|
|
const QString hostname = client.hostname();
|
|
const int port = client.port();
|
|
const QString clientId = client.clientId();
|
|
const quint16 keepAlive = client.keepAlive();
|
|
const bool clean = client.cleanSession();
|
|
|
|
client.connectToHost();
|
|
QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker.");
|
|
|
|
client.setClientId(QLatin1String("someclient"));
|
|
QCOMPARE(client.clientId(), clientId);
|
|
|
|
client.setHostname(QLatin1String("some.domain.foo"));
|
|
QCOMPARE(client.hostname(), hostname);
|
|
|
|
client.setPort(1234);
|
|
QCOMPARE(client.port(), port);
|
|
|
|
client.setKeepAlive(keepAlive + 10);
|
|
QCOMPARE(client.keepAlive(), keepAlive);
|
|
|
|
client.setProtocolVersion(QMqttClient::MQTT_3_1);
|
|
QCOMPARE(client.protocolVersion(), clientVersion);
|
|
|
|
client.setUsername(QLatin1String("someUser"));
|
|
QCOMPARE(client.username(), QLatin1String());
|
|
|
|
client.setPassword(QLatin1String("somePassword"));
|
|
QCOMPARE(client.password(), QLatin1String());
|
|
|
|
client.setCleanSession(!clean);
|
|
QCOMPARE(client.cleanSession(), clean);
|
|
}
|
|
|
|
void Tst_QMqttClient::authentication()
|
|
{
|
|
VersionClient(QMqttClient::MQTT_5_0, client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
|
|
QMqttConnectionProperties connectionProperties;
|
|
connectionProperties.setAuthenticationMethod(QLatin1String("SCRAM-SHA-1"));
|
|
client.setConnectionProperties(connectionProperties);
|
|
|
|
connect(&client, &QMqttClient::authenticationRequested, &client,
|
|
[](const QMqttAuthenticationProperties &prop)
|
|
{
|
|
qDebug() << "Authentication requested:" << prop.authenticationMethod();
|
|
});
|
|
|
|
connect(&client, &QMqttClient::authenticationFinished, &client,
|
|
[](const QMqttAuthenticationProperties &prop)
|
|
{
|
|
qDebug() << "Authentication finished:" << prop.authenticationMethod();
|
|
});
|
|
|
|
// ### FIXME : There is no public test broker yet able to handle authentication methods
|
|
// Theoretically the broker should send an AUTH request, followed by AUTH call including
|
|
// authentication data. See 4.12 of MQTT v5 specs.
|
|
QSKIP("No broker available with enhanced authentication.");
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
}
|
|
|
|
Q_DECLARE_METATYPE(QMqtt::MessageStatus)
|
|
|
|
void Tst_QMqttClient::messageStatus_data()
|
|
{
|
|
QTest::addColumn<int>("qos");
|
|
QTest::addColumn<QList<QMqtt::MessageStatus>>("expectedStatus");
|
|
|
|
QTest::newRow("QoS1") << 1 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Acknowledged);
|
|
QTest::newRow("QoS2") << 2 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Received
|
|
<< QMqtt::MessageStatus::Completed);
|
|
}
|
|
|
|
void Tst_QMqttClient::messageStatus()
|
|
{
|
|
QFETCH(int, qos);
|
|
QFETCH(QList<QMqtt::MessageStatus>, expectedStatus);
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
|
|
client.connectToHost();
|
|
|
|
QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker.");
|
|
|
|
const QString topic = QLatin1String("Qt/client/statusCheck");
|
|
|
|
connect(&client, &QMqttClient::messageStatusChanged,
|
|
&client, [&expectedStatus](qint32,
|
|
QMqtt::MessageStatus s,
|
|
const QMqttMessageStatusProperties &)
|
|
{
|
|
QCOMPARE(s, expectedStatus.first());
|
|
expectedStatus.takeFirst();
|
|
});
|
|
|
|
QSignalSpy publishSpy(&client, &QMqttClient::messageSent);
|
|
client.publish(topic, QByteArray("someContent"), quint8(qos));
|
|
QTRY_VERIFY2(publishSpy.size() == 1, "Could not publish message");
|
|
QTRY_VERIFY2(expectedStatus.isEmpty(), "Did not receive all status updates.");
|
|
}
|
|
|
|
void Tst_QMqttClient::messageStatusReceive_data()
|
|
{
|
|
QTest::addColumn<int>("qos");
|
|
QTest::addColumn<QList<QMqtt::MessageStatus>>("expectedStatus");
|
|
|
|
QTest::newRow("QoS1") << 1 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Published);
|
|
QTest::newRow("QoS2") << 2 << (QList<QMqtt::MessageStatus>() << QMqtt::MessageStatus::Published
|
|
<< QMqtt::MessageStatus::Released);
|
|
}
|
|
|
|
void Tst_QMqttClient::messageStatusReceive()
|
|
{
|
|
QFETCH(int, qos);
|
|
QFETCH(QList<QMqtt::MessageStatus>, expectedStatus);
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, publisher);
|
|
publisher.setHostname(m_testBroker);
|
|
publisher.setPort(m_port);
|
|
|
|
publisher.connectToHost();
|
|
QTRY_VERIFY2(publisher.state() == QMqttClient::Connected, "Could not connect to broker.");
|
|
|
|
VersionClient(QMqttClient::MQTT_5_0, subscriber);
|
|
subscriber.setHostname(m_testBroker);
|
|
subscriber.setPort(m_port);
|
|
|
|
subscriber.connectToHost();
|
|
QTRY_VERIFY2(subscriber.state() == QMqttClient::Connected, "Could not connect to broker.");
|
|
|
|
const QString topic = QLatin1String("Qt/client/statusCheckReceive");
|
|
|
|
auto subscription = subscriber.subscribe(topic, quint8(qos));
|
|
QTRY_VERIFY2(subscription->state() == QMqttSubscription::Subscribed, "Could not subscribe to topic");
|
|
QVERIFY(subscription->qos() >= qos);
|
|
|
|
connect(&subscriber, &QMqttClient::messageStatusChanged, &subscriber,
|
|
[&expectedStatus](qint32, QMqtt::MessageStatus s, const QMqttMessageStatusProperties &)
|
|
{
|
|
QCOMPARE(s, expectedStatus.first());
|
|
expectedStatus.takeFirst();
|
|
});
|
|
|
|
QSignalSpy publishSpy(&publisher, &QMqttClient::messageSent);
|
|
QSignalSpy receiveSpy(&subscriber, &QMqttClient::messageReceived);
|
|
|
|
publisher.publish(topic, QByteArray("someContent"), quint8(qos));
|
|
QTRY_VERIFY2(publishSpy.size() == 1, "Could not publish message");
|
|
QTRY_VERIFY2(receiveSpy.size() == 1, "Did not receive message");
|
|
|
|
QTRY_VERIFY2(expectedStatus.isEmpty(), "Did not receive all status updates.");
|
|
}
|
|
|
|
void Tst_QMqttClient::subscriptionIdsOverlap()
|
|
{
|
|
|
|
// If the Server sends a single copy of the message it MUST include in the
|
|
// PUBLISH packet the Subscription Identifiers for all matching
|
|
// subscriptions which have a Subscription Identifiers, their order is not
|
|
// significant [MQTT-3.3.4-4].
|
|
// If the Server sends multiple PUBLISH packets it MUST send, in each of
|
|
// them, the Subscription Identifier of the matching subscription if it has
|
|
// a Subscription Identifier [MQTT-3.3.4-5].
|
|
|
|
const QString topic = QLatin1String("Qt/client/idcheck");
|
|
// Connect publisher
|
|
VersionClient(QMqttClient::MQTT_5_0, pub);
|
|
pub.setHostname(m_testBroker);
|
|
pub.setPort(m_port);
|
|
|
|
pub.connectToHost();
|
|
QTRY_VERIFY2(pub.state() == QMqttClient::Connected, "Could not connect publisher.");
|
|
|
|
// Connect subA
|
|
VersionClient(QMqttClient::MQTT_5_0, subClientA);
|
|
subClientA.setHostname(m_testBroker);
|
|
subClientA.setPort(m_port);
|
|
|
|
subClientA.connectToHost();
|
|
QTRY_VERIFY2(subClientA.state() == QMqttClient::Connected, "Could not connect subscriber A.");
|
|
|
|
QMqttSubscriptionProperties subAProp;
|
|
subAProp.setSubscriptionIdentifier(8);
|
|
auto subA = subClientA.subscribe(topic, subAProp, 1);
|
|
QTRY_VERIFY2(subA->state() == QMqttSubscription::Subscribed, "Could not subscibe A.");
|
|
|
|
int receiveACounter = 0;
|
|
connect(subA, &QMqttSubscription::messageReceived, subA,
|
|
[&receiveACounter](QMqttMessage msg) {
|
|
qDebug() << "Sub A received:" << msg.publishProperties().subscriptionIdentifiers();
|
|
// ### TODO: Wait for fix at https://github.com/eclipse/paho.mqtt.testing/issues/56
|
|
//QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() == 1);
|
|
//QVERIFY(msg.publishProperties().subscriptionIdentifiers().at(0) == 8); // Use sub->id();
|
|
receiveACounter++;
|
|
});
|
|
|
|
// Connect subB
|
|
VersionClient(QMqttClient::MQTT_5_0, subClientB);
|
|
subClientB.setHostname(m_testBroker);
|
|
subClientB.setPort(m_port);
|
|
|
|
subClientB.connectToHost();
|
|
QTRY_VERIFY2(subClientB.state() == QMqttClient::Connected, "Could not connect subscriber A.");
|
|
|
|
QMqttSubscriptionProperties subBProp;
|
|
subBProp.setSubscriptionIdentifier(9);
|
|
auto subB = subClientB.subscribe(topic, subBProp, 1);
|
|
QTRY_VERIFY2(subB->state() == QMqttSubscription::Subscribed, "Could not subscibe A.");
|
|
|
|
int receiveBCounter = 2;
|
|
connect(subB, &QMqttSubscription::messageReceived,
|
|
subB, [&receiveBCounter](QMqttMessage msg) {
|
|
qDebug() << "Sub B received:" << msg.publishProperties().subscriptionIdentifiers();
|
|
QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0);
|
|
receiveBCounter -= msg.publishProperties().subscriptionIdentifiers().size();
|
|
});
|
|
|
|
QMqttSubscriptionProperties subB2Prop;
|
|
subB2Prop.setSubscriptionIdentifier(14);
|
|
auto subB2 = subClientB.subscribe(topic + "/#", subB2Prop, 1);
|
|
QTRY_VERIFY2(subB2->state() == QMqttSubscription::Subscribed, "Could not subscibe A.");
|
|
|
|
int receiveB2Counter = 2;
|
|
connect(subB2, &QMqttSubscription::messageReceived,
|
|
subB2, [&receiveB2Counter](QMqttMessage msg) {
|
|
qDebug() << "Sub B2 received:" << msg.publishProperties().subscriptionIdentifiers();
|
|
QVERIFY(msg.publishProperties().subscriptionIdentifiers().size() > 0);
|
|
receiveB2Counter -= msg.publishProperties().subscriptionIdentifiers().size();
|
|
});
|
|
|
|
QSignalSpy publishSpy(&pub, &QMqttClient::messageSent);
|
|
pub.publish(topic, "SomeData", 1);
|
|
QTRY_VERIFY2(publishSpy.size() == 1, "Could not finalize publication.");
|
|
QTRY_VERIFY2(receiveBCounter == 0, "Did not receive both messages.");
|
|
QTRY_VERIFY2(receiveB2Counter == 0, "Did not receive both messages.");
|
|
QTRY_VERIFY2(receiveACounter == 1, "Did not receive non-overlapping message.");
|
|
}
|
|
|
|
DefaultVersionTestData(Tst_QMqttClient::keepAlive_data)
|
|
|
|
void Tst_QMqttClient::keepAlive()
|
|
{
|
|
QFETCH(QMqttClient::ProtocolVersion, mqttVersion);
|
|
const quint16 keepAlive = 1;
|
|
|
|
VersionClient(mqttVersion, client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
|
|
client.setKeepAlive(keepAlive);
|
|
QCOMPARE(client.keepAlive(), keepAlive);
|
|
QCOMPARE(client.autoKeepAlive(), true); // default
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
if (client.protocolVersion() == QMqttClient::MQTT_5_0) {
|
|
const auto serverProps = client.serverConnectionProperties();
|
|
if (serverProps.isValid() && serverProps.availableProperties() & QMqttServerConnectionProperties::ServerKeepAlive) {
|
|
if (serverProps.serverKeepAlive() == 0 || serverProps.serverKeepAlive() > keepAlive) {
|
|
qDebug() << "Server specifies keepAlive which cannot be used for this test";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Changing autoKeepAlive is not possible while connected
|
|
client.setAutoKeepAlive(false);
|
|
QCOMPARE(client.autoKeepAlive(), true);
|
|
|
|
// Sending a manual ping request with autoKeepAlive is not allowed
|
|
// and will be suppressed.
|
|
const bool sent = client.requestPing();
|
|
QCOMPARE(sent, false);
|
|
|
|
// Verify auto keep alive works
|
|
QSignalSpy spy(&client, &QMqttClient::pingResponseReceived);
|
|
QTRY_VERIFY_WITH_TIMEOUT(spy.size() == 5, keepAlive * 1000 * 5 * 2);
|
|
spy.clear();
|
|
|
|
client.disconnectFromHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
|
|
// Set manual keepAlive
|
|
client.setAutoKeepAlive(false);
|
|
QCOMPARE(client.autoKeepAlive(), false);
|
|
client.setKeepAlive(keepAlive);
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
// Check for timeout / disconnect when not ping sent
|
|
QTRY_COMPARE_WITH_TIMEOUT(client.state(), QMqttClient::Disconnected, keepAlive * 1000 * 5 * 2);
|
|
|
|
// Check manual ping
|
|
QTimer t;
|
|
t.setInterval(keepAlive * 1000);
|
|
t.setSingleShot(false);
|
|
t.connect(&t, &QTimer::timeout, &client, [&client]() {
|
|
client.requestPing();
|
|
});
|
|
|
|
connect(&client, &QMqttClient::connected, &t, [&t]() {
|
|
t.start();
|
|
});
|
|
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
QTRY_VERIFY_WITH_TIMEOUT(spy.size() == 5, keepAlive * 1000 * 5 * 2);
|
|
|
|
client.disconnectFromHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
}
|
|
|
|
void Tst_QMqttClient::reconnect()
|
|
{
|
|
VersionClient(QMqttClient::ProtocolVersion(0), client);
|
|
client.setHostname(m_testBroker);
|
|
client.setPort(m_port);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
client.disconnectFromHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
|
|
/* The rest requires a mosquitto installation that runs
|
|
on multiple ports.
|
|
*/
|
|
#if defined(MQTT_TEST_BROKER)
|
|
client.setPort(client.port() + 1000);
|
|
client.connectToHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Connected);
|
|
|
|
client.disconnectFromHost();
|
|
QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
|
|
|
|
QMqttClient client2;
|
|
client2.setHostname(client.hostname());
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
#if defined(QT_MQTT_WITH_WEBSOCKETS)
|
|
client2.setPort(9080);
|
|
client2.connectToHostWebSocket();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Connected);
|
|
|
|
client2.disconnectFromHost();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Disconnected);
|
|
#endif
|
|
|
|
#if defined(QT_MQTT_WITH_WEBSOCKETS) && !defined(QT_NO_SSL)
|
|
client2.setPort(9081);
|
|
QUrl url(QString(u"wss://") + client2.hostname() + QString(u":") + QString::number(client2.port()));
|
|
QWebSocketHandshakeOptions options;
|
|
options.setSubprotocols(QStringList{ QString::fromUtf8("mqttv3.1") });
|
|
QWebSocket socket;
|
|
socket.setSslConfiguration(makeConfig(QString::fromUtf8(getenv("MQTT_TEST_BROKER_CERTIFICATE"))));
|
|
socket.open(url, options);
|
|
|
|
client2.connectToHostWebSocketEncrypted(&socket);
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Connected);
|
|
|
|
client2.disconnectFromHost();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Disconnected);
|
|
#endif
|
|
|
|
#if !defined(QT_NO_SSL)
|
|
client2.setPort(9883);
|
|
client2.connectToHostEncrypted(makeConfig(QString::fromUtf8(getenv("MQTT_TEST_BROKER_CERTIFICATE"))));
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Connected);
|
|
|
|
client2.disconnectFromHost();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Disconnected);
|
|
#endif
|
|
|
|
client2.setPort(2883);
|
|
client2.connectToHost();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Connected);
|
|
|
|
client2.disconnectFromHost();
|
|
QTRY_COMPARE(client2.state(), QMqttClient::Disconnected);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
QTEST_MAIN(Tst_QMqttClient)
|
|
|
|
#include "tst_qmqttclient.moc"
|