qtcoap/tests/auto/qcoapclient/tst_qcoapclient.cpp

1103 lines
38 KiB
C++

// Copyright (C) 2017 Witekio.
// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest>
#include <QCoreApplication>
#include <QtCoap/qcoapclient.h>
#include <QtCoap/qcoaprequest.h>
#include <QtCoap/qcoapreply.h>
#include <QtCoap/qcoapresourcediscoveryreply.h>
#include <QtCore/qbuffer.h>
#include <QtNetwork/qnetworkdatagram.h>
#include <QtNetwork/qsslcipher.h>
#include <private/qcoapclient_p.h>
#include <private/qcoapqudpconnection_p.h>
#include <private/qcoapprotocol_p.h>
#include <private/qcoaprequest_p.h>
#include "../coapnetworksettings.h"
using namespace QtCoapNetworkSettings;
class tst_QCoapClient : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void incorrectUrls_data();
void incorrectUrls();
void methods_data();
void methods();
void separateMethod();
void socketError();
void timeout_data();
void timeout();
void abort();
void removeReply();
void setBlockSize_data();
void setBlockSize();
void requestWithQIODevice_data();
void requestWithQIODevice();
void multipleRequests_data();
void multipleRequests();
void blockwiseReply_data();
void blockwiseReply();
void blockwiseRequest_data();
void blockwiseRequest();
void discover_data();
void discover();
void observe_data();
void observe();
void confirmableMulticast();
void multicast();
void multicast_blockwise();
void setMinimumTokenSize_data();
void setMinimumTokenSize();
};
class QCoapClientForSecurityTests : public QCoapClient
{
public:
QCoapClientForSecurityTests(QtCoap::SecurityMode security)
: QCoapClient(security)
, securityMode(security)
{
if (security != QtCoap::SecurityMode::NoSecurity)
setSecurityConfiguration(createConfiguration(security));
}
bool securitySetupMissing() const
{
#if QT_CONFIG(dtls)
if (securityMode == QtCoap::SecurityMode::PreSharedKey) {
// The Californium test server accepts only PSK-AES128-CCM8 and PSK-AES128-CBC-SHA256
// ciphers. Make sure that the required ciphers are present, otherwise the test
// should be skipped.
const auto ciphers = QSslConfiguration::defaultDtlsConfiguration().ciphers();
const auto it = std::find_if(ciphers.cbegin(), ciphers.cend(),
[](const QSslCipher &cipher) {
return cipher.name() == "PSK-AES128-CCM8"
|| cipher.name() == "PSK-AES128-CBC-SHA256";
});
return it == ciphers.cend();
}
// For all other modes the setup should be OK, return false.
return false;
#else
// If dtls is not configured, the setup for the secure modes is missing,
// but it is OK if security is not used.
return isSecure();
#endif
}
bool isSecure() const
{
return securityMode != QtCoap::SecurityMode::NoSecurity;
}
private:
QtCoap::SecurityMode securityMode;
};
#ifdef QT_BUILD_INTERNAL
class QCoapQUdpConnectionSocketTestsPrivate : public QCoapQUdpConnectionPrivate
{
bool bind() override
{
// Force a socket binding error
QUdpSocket anotherSocket;
anotherSocket.bind(QHostAddress::Any, 6080);
return socket()->bind(QHostAddress::Any, 6080);
}
};
class QCoapQUdpConnectionSocketTests : public QCoapQUdpConnection
{
public:
QCoapQUdpConnectionSocketTests() :
QCoapQUdpConnection(*new QCoapQUdpConnectionSocketTestsPrivate)
{
createSocket();
}
private:
Q_DECLARE_PRIVATE(QCoapQUdpConnectionSocketTests)
};
class QCoapClientForSocketErrorTests : public QCoapClient
{
public:
QCoapClientForSocketErrorTests()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
privateClient->setConnection(new QCoapQUdpConnectionSocketTests());
}
QCoapQUdpConnection *connection()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
return qobject_cast<QCoapQUdpConnection*>(privateClient->connection);
}
};
class QCoapClientForTests : public QCoapClient
{
public:
QCoapClientForTests() {}
QCoapProtocol *protocol()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
return privateClient->protocol;
}
QCoapQUdpConnection *connection()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
return qobject_cast<QCoapQUdpConnection*>(privateClient->connection);
}
};
class QCoapConnectionMulticastTests : public QCoapConnection
{
public:
~QCoapConnectionMulticastTests() override = default;
void bind(const QString &host, quint16 port) override
{
Q_UNUSED(host)
Q_UNUSED(port)
// Do nothing
}
void writeData(const QByteArray &data, const QString &host, quint16 port) override
{
Q_UNUSED(data)
Q_UNUSED(host)
Q_UNUSED(port)
// Do nothing
}
void close() override {}
};
class QCoapClientForMulticastTests : public QCoapClient
{
public:
QCoapClientForMulticastTests()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
privateClient->setConnection(new QCoapConnectionMulticastTests());
}
QCoapConnection *connection()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
return privateClient->connection;
}
};
#endif
class Helper : public QObject
{
Q_OBJECT
public:
Helper() {}
public slots:
void onError(QCoapReply *, QtCoap::Error error)
{
qWarning() << "Network error" << error << "occurred";
}
};
void tst_QCoapClient::initTestCase()
{
#if defined(COAP_TEST_SERVER_IP) || defined(QT_TEST_SERVER)
QVERIFY2(waitForHost(testServerHost()), "Failed to connect to Californium plugtest server.");
#if QT_CONFIG(dtls)
QVERIFY2(waitForHost(timeServerUrl(), QtCoap::SecurityMode::Certificate),
"Failed to connect to FreeCoAP sample time server.");
#endif
#endif
}
void tst_QCoapClient::incorrectUrls_data()
{
qWarning("Expect warnings here...");
QTest::addColumn<QUrl>("url");
QTest::newRow("get") << QUrl("wrong://10.20.30.40:5683/test");
QTest::newRow("post") << QUrl("wrong://10.20.30.40:5683/test");
QTest::newRow("put") << QUrl("wrong://10.20.30.40:5683/test");
QTest::newRow("delete") << QUrl("wrong://10.20.30.40:5683/test");
QTest::newRow("discover") << QUrl("wrong://10.20.30.40:5683/test");
}
void tst_QCoapClient::incorrectUrls()
{
QFETCH(QUrl, url);
QCoapClient client;
QScopedPointer<QCoapReply> reply;
if (qstrcmp(QTest::currentDataTag(), "get") == 0)
reply.reset(client.get(url));
else if (qstrcmp(QTest::currentDataTag(), "post") == 0)
reply.reset(client.post(url));
else if (qstrcmp(QTest::currentDataTag(), "put") == 0)
reply.reset(client.put(url));
else if (qstrcmp(QTest::currentDataTag(), "delete") == 0)
reply.reset(client.deleteResource(url));
else if (qstrcmp(QTest::currentDataTag(), "discover") == 0)
reply.reset(client.discover(url));
else {
QString error = QLatin1String("Unrecognized method '") + QTest::currentDataTag() + "'";
QFAIL(qPrintable(error));
}
QVERIFY2(reply.isNull(), "Request did not fail as expected.");
}
void tst_QCoapClient::methods_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<QtCoap::Method>("method");
QTest::addColumn<QtCoap::SecurityMode>("security");
QTest::newRow("get")
<< QUrl(testServerResource())
<< QtCoap::Method::Get
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_no_port")
<< QUrl("coap://" + testServerHost() + "/test")
<< QtCoap::Method::Get
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_no_scheme_no_port")
<< QUrl(testServerHost() + "/test")
<< QtCoap::Method::Get
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_psk")
<< QUrl(testServerResource()) << QtCoap::Method::Get
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("get_cert")
<< QUrl(timeServerUrl()) << QtCoap::Method::Get
<< QtCoap::SecurityMode::Certificate;
QTest::newRow("post")
<< QUrl(testServerResource())
<< QtCoap::Method::Post
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("post_no_scheme_no_port")
<< QUrl(testServerHost() + "/test")
<< QtCoap::Method::Post
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("post_psk")
<< QUrl(testServerResource()) << QtCoap::Method::Post
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("put")
<< QUrl(testServerResource())
<< QtCoap::Method::Put
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("put_no_scheme_no_port")
<< QUrl(testServerHost() + "/test")
<< QtCoap::Method::Put
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("put_psk")
<< QUrl(testServerResource()) << QtCoap::Method::Put
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("delete")
<< QUrl(testServerResource())
<< QtCoap::Method::Delete
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("delete_no_scheme_no_port")
<< QUrl(testServerHost() + "/test")
<< QtCoap::Method::Delete
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("delete_psk")
<< QUrl(testServerResource()) << QtCoap::Method::Delete
<< QtCoap::SecurityMode::PreSharedKey;
}
void tst_QCoapClient::methods()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QFETCH(QtCoap::Method, method);
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
QCoapRequest request(url);
QSignalSpy spyClientFinished(&client, &QCoapClient::finished);
const QByteArray payload = "test payload";
QScopedPointer<QCoapReply> reply;
if (qstrncmp(QTest::currentDataTag(), "get", 3) == 0) {
reply.reset(client.get(request));
} else if (qstrncmp(QTest::currentDataTag(), "post", 4) == 0) {
request.setPayload(payload);
reply.reset(client.post(request));
} else if (qstrncmp(QTest::currentDataTag(), "put", 3) == 0) {
request.setPayload(payload);
reply.reset(client.put(request));
} else if (qstrncmp(QTest::currentDataTag(), "delete", 6) == 0) {
reply.reset(client.deleteResource(request));
} else {
QString error = QLatin1String("Unrecognized method '") + QTest::currentDataTag() + "'";
QFAIL(qPrintable(error));
}
QVERIFY2(!reply.isNull(), "Request failed unexpectedly");
#ifdef QT_BUILD_INTERNAL
QCOMPARE(reply->url(),
QCoapRequestPrivate::adjustedUrl(url, client.isSecure()));
#endif
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QTRY_COMPARE(spyReplyFinished.size(), 1);
QTRY_COMPARE(spyClientFinished.size(), 1);
QByteArray replyData;
if (!reply.isNull()) {
replyData = reply->readAll();
if (qstrncmp(QTest::currentDataTag(), "get", 3) == 0) {
QVERIFY(!replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Content);
} else if (qstrncmp(QTest::currentDataTag(), "post", 4) == 0) {
QVERIFY(replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Created);
QCOMPARE(reply->request().payload(), payload);
} else if (qstrncmp(QTest::currentDataTag(), "put", 3) == 0) {
QVERIFY(replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Changed);
QCOMPARE(reply->request().payload(), payload);
} else if (qstrncmp(QTest::currentDataTag(), "delete", 6) == 0) {
QVERIFY(replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Deleted);
} else {
QString error = QLatin1String("Unrecognized method '") + QTest::currentDataTag() + "'";
QFAIL(qPrintable(error));
}
}
QCOMPARE(reply->request().method(), method);
}
void tst_QCoapClient::separateMethod()
{
CHECK_FOR_COAP_SERVER;
QCoapClient client;
QScopedPointer<QCoapReply> reply(client.get(QUrl(testServerUrl() + "/separate")));
QVERIFY2(!reply.isNull(), "Request failed unexpectedly");
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QTRY_COMPARE(spyReplyFinished.size(), 1);
QByteArray replyData = reply->readAll();
QVERIFY(!replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Content);
}
void tst_QCoapClient::removeReply()
{
CHECK_FOR_COAP_SERVER;
QCoapClient client;
QCoapReply *reply = client.get(QUrl(testServerResource()));
QVERIFY2(reply != nullptr, "Request failed unexpectedly");
connect(reply, &QCoapReply::finished, this, [reply]() { reply->deleteLater(); });
try {
QEventLoop eventLoop;
QTimer::singleShot(2000, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
} catch (...) {
QFAIL("Exception occurred after destroying the QCoapReply");
}
}
void tst_QCoapClient::setBlockSize_data()
{
QTest::addColumn<int>("blockSizeSet");
QTest::addColumn<int>("blockSizeExpected");
QTest::newRow("valid_size_0") << 0 << 0;
QTest::newRow("valid_size_16") << 16 << 16;
QTest::newRow("valid_size_1024") << 1024 << 1024;
QTest::newRow("invalid_size_8") << 8 << 0;
QTest::newRow("invalid_size_350") << 350 << 0;
QTest::newRow("invalid_size_2048") << 2048 << 0;
}
void tst_QCoapClient::setBlockSize()
{
#ifdef QT_BUILD_INTERNAL
QFETCH(int, blockSizeSet);
QFETCH(int, blockSizeExpected);
QCoapClientForTests client;
client.setBlockSize(static_cast<quint16>(blockSizeSet));
QEventLoop eventLoop;
QTimer::singleShot(1000, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
QCOMPARE(client.protocol()->blockSize(), blockSizeExpected);
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
void tst_QCoapClient::requestWithQIODevice_data()
{
QTest::addColumn<QUrl>("url");
QTest::newRow("post") << QUrl(testServerResource());
QTest::newRow("put") << QUrl(testServerResource());
}
void tst_QCoapClient::requestWithQIODevice()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QCoapClient client;
QCoapRequest request(url);
QBuffer buffer;
buffer.open(QIODevice::ReadWrite);
buffer.write("Some data");
QScopedPointer<QCoapReply> reply;
if (qstrcmp(QTest::currentDataTag(), "post") == 0)
reply.reset(client.post(request, &buffer));
else
reply.reset(client.put(request, &buffer));
QVERIFY2(!reply.isNull(), "Request failed unexpectedly");
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QTRY_COMPARE(spyReplyFinished.size(), 1);
QByteArray replyData = reply->readAll();
if (qstrcmp(QTest::currentDataTag(), "post") == 0) {
QVERIFY(replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Created);
} else if (qstrcmp(QTest::currentDataTag(), "put") == 0) {
QVERIFY(replyData.isEmpty());
QCOMPARE(reply->responseCode(), QtCoap::ResponseCode::Changed);
}
}
void tst_QCoapClient::multipleRequests_data()
{
QTest::addColumn<QtCoap::SecurityMode>("security");
QTest::newRow("multiple_requests") << QtCoap::SecurityMode::NoSecurity;
QTest::newRow("multiple_requests_secure") << QtCoap::SecurityMode::PreSharedKey;
}
void tst_QCoapClient::multipleRequests()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
QUrl url = QUrl(testServerResource());
QSignalSpy spyClientFinished(&client, &QCoapClient::finished);
const uint8_t requestCount = 4;
QList<QSharedPointer<QCoapReply>> replies;
QList<QSharedPointer<QSignalSpy>> signalSpies;
for (uint8_t i = 0; i < requestCount; ++i) {
QCoapRequest request;
const auto token = "token" + QByteArray::number(i);
request.setToken(token);
request.setUrl(url);
QSharedPointer<QCoapReply> reply(client.get(request));
const auto errorMsg = QStringLiteral("Request number %1 failed unexpectedly").arg(i);
QVERIFY2(!reply.isNull(), qPrintable(errorMsg));
replies.push_back(reply);
QSharedPointer<QSignalSpy> signalSpy(
new QSignalSpy(reply.data(), &QCoapReply::finished));
signalSpies.push_back(signalSpy);
}
for (const auto &signalSpy : signalSpies)
QTRY_COMPARE(signalSpy->size(), 1);
QTRY_COMPARE(spyClientFinished.size(), 4);
for (uint8_t i = 0; i < requestCount; ++i) {
QCOMPARE(replies[i]->responseCode(), QtCoap::ResponseCode::Content);
QByteArray replyData = replies[i]->readAll();
const auto token = "token" + QByteArray::number(i);
// The californium server now returns the hex token in uppercase
QVERIFY(replyData.contains(token.toHex().toUpper()));
}
}
void tst_QCoapClient::socketError()
{
#ifdef QT_BUILD_INTERNAL
CHECK_FOR_COAP_SERVER;
QCoapClientForSocketErrorTests client;
QUrl url = QUrl(testServerResource());
const auto connection = client.connection();
QVERIFY2(connection, "Failed to get coap connection!");
QUdpSocket *socket = connection->socket();
QVERIFY2(socket, "Socket not properly created with connection");
QSignalSpy spySocketError(socket, &QUdpSocket::errorOccurred);
QScopedPointer<QCoapReply> reply(client.get(url));
QSignalSpy spyClientError(&client, &QCoapClient::error);
QTRY_COMPARE_WITH_TIMEOUT(spySocketError.size(), 1, 10000);
QTRY_COMPARE_WITH_TIMEOUT(spyClientError.size(), 1, 1000);
QCOMPARE(qvariant_cast<QtCoap::Error>(spyClientError.first().at(1)),
QtCoap::Error::AddressInUse);
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
void tst_QCoapClient::timeout_data()
{
QTest::addColumn<uint>("timeout");
QTest::addColumn<uint>("maximumRetransmitCount");
QTest::newRow("2000/0") << 2000u << 0u;
QTest::newRow("2000/2") << 2000u << 2u;
QTest::newRow("4000/0") << 4000u << 0u;
}
void tst_QCoapClient::timeout()
{
#ifdef QT_BUILD_INTERNAL
CHECK_FOR_COAP_SERVER;
QFETCH(uint, timeout);
QFETCH(uint, maximumRetransmitCount);
QCoapClientForTests client;
// Trigger a network timeout
client.protocol()->setAckTimeout(timeout);
client.protocol()->setAckRandomFactor(1);
client.protocol()->setMaximumRetransmitCount(maximumRetransmitCount);
QUrl url = QUrl("coap://192.0.2.0:5683/"); // Need an url that returns nothing
QElapsedTimer timeoutTimer;
timeoutTimer.start();
QScopedPointer<QCoapReply> reply(
client.get(QCoapRequest(url, QCoapMessage::Type::Confirmable)));
QSignalSpy spyClientError(&client, &QCoapClient::error);
QSignalSpy spyReplyError(reply.data(), &QCoapReply::error);
QSignalSpy spyReplyAborted(reply.data(), &QCoapReply::aborted);
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
// Check timeout upper limit
uint transmissions = maximumRetransmitCount + 1;
// 10% Precision expected at least, plus timer precision
QTRY_COMPARE_WITH_TIMEOUT(spyReplyError.size(), 1, static_cast<int>(
1.1 * client.protocol()->maximumTransmitWait() + 20 * transmissions));
// Check timeout lower limit
qint64 elapsedTime = timeoutTimer.elapsed();
QString errorMessage = QString("Timeout was triggered after %1ms, while expecting about %2ms")
.arg(QString::number(elapsedTime),
QString::number(client.protocol()->maximumTransmitWait()));
// 10% Precision expected at least, minus timer precision
QVERIFY2(elapsedTime > 0.9 * client.protocol()->maximumTransmitWait() - 20 * transmissions,
qPrintable(errorMessage));
QCOMPARE(qvariant_cast<QtCoap::Error>(spyReplyError.first().at(1)),
QtCoap::Error::TimeOut);
QCOMPARE(spyReplyFinished.size(), 1);
QCOMPARE(spyReplyAborted.size(), 0);
QCOMPARE(spyClientError.size(), 1);
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
void tst_QCoapClient::abort()
{
CHECK_FOR_COAP_SERVER;
QCoapClient client;
QUrl url = QUrl(testServerUrl() + "/large");
QScopedPointer<QCoapReply> reply(client.get(url));
QVERIFY(!reply.isNull());
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QSignalSpy spyReplyAborted(reply.data(), &QCoapReply::aborted);
QSignalSpy spyReplyError(reply.data(), &QCoapReply::error);
reply->abortRequest();
QEventLoop eventLoop;
QTimer::singleShot(1000, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
QCOMPARE(spyReplyAborted.size(), 1);
QCOMPARE(spyReplyFinished.size(), 1);
QCOMPARE(spyReplyError.size(), 0);
}
void tst_QCoapClient::blockwiseReply_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<QCoapMessage::Type>("type");
QTest::addColumn<QByteArray>("replyData");
QTest::addColumn<QtCoap::SecurityMode>("security");
QByteArray data;
data.append("/-------------------------------------------------------------\\\n");
data.append("| RESOURCE BLOCK NO. 1 OF 5 |\n");
data.append("| [each line contains 64 bytes] |\n");
data.append("\\-------------------------------------------------------------/\n");
data.append("/-------------------------------------------------------------\\\n");
data.append("| RESOURCE BLOCK NO. 2 OF 5 |\n");
data.append("| [each line contains 64 bytes] |\n");
data.append("\\-------------------------------------------------------------/\n");
data.append("/-------------------------------------------------------------\\\n");
data.append("| RESOURCE BLOCK NO. 3 OF 5 |\n");
data.append("| [each line contains 64 bytes] |\n");
data.append("\\-------------------------------------------------------------/\n");
data.append("/-------------------------------------------------------------\\\n");
data.append("| RESOURCE BLOCK NO. 4 OF 5 |\n");
data.append("| [each line contains 64 bytes] |\n");
data.append("\\-------------------------------------------------------------/\n");
data.append("/-------------------------------------------------------------\\\n");
data.append("| RESOURCE BLOCK NO. 5 OF 5 |\n");
data.append("| [each line contains 64 bytes] |\n");
data.append("\\-------------------------------------------------------------/\n");
QTest::newRow("get_large")
<< QUrl(testServerUrl() + "/large")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_large_secure")
<< QUrl(testServerHost() + "/large")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("get_large_separate")
<< QUrl(testServerHost() + "/large-separate")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_large_confirmable")
<< QUrl(testServerUrl() + "/large")
<< QCoapMessage::Type::Confirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_large_confirmable_secure")
<< QUrl(testServerHost() + "/large")
<< QCoapMessage::Type::Confirmable
<< data
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("get_large_separate_confirmable")
<< QUrl(testServerUrl() + "/large-separate")
<< QCoapMessage::Type::Confirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_large_16bits")
<< QUrl(testServerUrl() + "/large")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("get_large_16bits_confirmable")
<< QUrl(testServerUrl() + "/large")
<< QCoapMessage::Type::Confirmable
<< data
<< QtCoap::SecurityMode::NoSecurity;
}
void tst_QCoapClient::blockwiseReply()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QFETCH(QCoapMessage::Type, type);
QFETCH(QByteArray, replyData);
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
QCoapRequest request(url);
if (qstrncmp(QTest::currentDataTag(), "get_large_16bits", 16) == 0)
client.setBlockSize(16);
request.setType(type);
QScopedPointer<QCoapReply> reply(client.get(request));
QVERIFY(!reply.isNull());
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QSignalSpy spyReplyError(reply.data(), &QCoapReply::error);
Helper helper;
connect(reply.data(), &QCoapReply::error, &helper, &Helper::onError);
QCOMPARE(spyReplyError.size(), 0);
QTRY_COMPARE_WITH_TIMEOUT(spyReplyFinished.size(), 1, 30000);
QCOMPARE(reply->readAll(), replyData);
}
void tst_QCoapClient::blockwiseRequest_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<QCoapMessage::Type>("type");
QTest::addColumn<QByteArray>("requestData");
QTest::addColumn<QtCoap::ResponseCode>("responseCode");
QTest::addColumn<QByteArray>("replyData");
QTest::addColumn<QtCoap::SecurityMode>("security");
QByteArray data;
const char alphabet[] = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
for (int i = 3; i-- > 0; )
data.append(alphabet);
QTest::newRow("large_post_empty_reply")
<< QUrl(testServerUrl() + "/query")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::ResponseCode::MethodNotAllowed
<< QByteArray()
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("large_post_large_reply")
<< QUrl(testServerUrl() + "/large-post")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::ResponseCode::Changed
<< data.toUpper()
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("large_post_large_reply_secure")
<< QUrl(testServerHost() + "/large-post")
<< QCoapMessage::Type::NonConfirmable
<< data
<< QtCoap::ResponseCode::Changed
<< data.toUpper()
<< QtCoap::SecurityMode::PreSharedKey;
}
void tst_QCoapClient::blockwiseRequest()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QFETCH(QCoapMessage::Type, type);
QFETCH(QByteArray, requestData);
QFETCH(QtCoap::ResponseCode, responseCode);
QFETCH(QByteArray, replyData);
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
client.setBlockSize(16);
QCoapRequest request(url);
request.setType(type);
request.addOption(QCoapOption::ContentFormat);
QScopedPointer<QCoapReply> reply(client.post(request, requestData));
QVERIFY(!reply.isNull());
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QTRY_COMPARE_WITH_TIMEOUT(spyReplyFinished.size(), 1, 30000);
QByteArray dataReply = reply->readAll();
QCOMPARE(dataReply, replyData);
QCOMPARE(reply->responseCode(), responseCode);
}
void tst_QCoapClient::discover_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<int>("resourceNumber");
QTest::addColumn<QtCoap::SecurityMode>("security");
// Californium test server exposes 31 resources
QTest::newRow("discover")
<< QUrl(testServerHost())
<< 31
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("discover_secure")
<< QUrl(testServerHost())
<< 31
<< QtCoap::SecurityMode::PreSharedKey;
}
void tst_QCoapClient::discover()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QFETCH(int, resourceNumber);
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
QScopedPointer<QCoapResourceDiscoveryReply> resourcesReply(client.discover(url)); // /.well-known/core
QVERIFY(!resourcesReply.isNull());
QSignalSpy spyReplyFinished(resourcesReply.data(), &QCoapReply::finished);
QTRY_COMPARE_WITH_TIMEOUT(spyReplyFinished.size(), 1, 30000);
const auto discoverUrl = QUrl(url.toString() + "/.well-known/core");
#ifdef QT_BUILD_INTERNAL
QCOMPARE(resourcesReply->url(),
QCoapRequestPrivate::adjustedUrl(discoverUrl, client.isSecure()));
#endif
QCOMPARE(resourcesReply->resources().size(), resourceNumber);
QCOMPARE(resourcesReply->request().method(), QtCoap::Method::Get);
//! TODO Test discovery content too
}
void tst_QCoapClient::observe_data()
{
qWarning("Observe tests may take some time, don't forget to raise Tests timeout in settings.");
QTest::addColumn<QUrl>("url");
QTest::addColumn<QCoapMessage::Type>("type");
QTest::addColumn<QtCoap::SecurityMode>("security");
QTest::newRow("observe")
<< QUrl(testServerUrl() + "/obs")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_secure")
<< QUrl(testServerHost() + "/obs")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("observe_no_scheme_no_port")
<< QUrl(testServerHost() + "/obs")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_confirmable")
<< QUrl(testServerUrl() + "/obs")
<< QCoapMessage::Type::Confirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_receive")
<< QUrl(testServerUrl() + "/obs-non")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_receive_confirmable")
<< QUrl(testServerUrl() + "/obs-non")
<< QCoapMessage::Type::Confirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_large")
<< QUrl(testServerUrl() + "/obs-large")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_large_secure")
<< QUrl(testServerHost() + "/obs-large")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::PreSharedKey;
QTest::newRow("observe_large_confirmable")
<< QUrl(testServerUrl() + "/obs-large")
<< QCoapMessage::Type::Confirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_pumping")
<< QUrl(testServerUrl() + "/obs-pumping")
<< QCoapMessage::Type::NonConfirmable
<< QtCoap::SecurityMode::NoSecurity;
QTest::newRow("observe_pumping_confirmable")
<< QUrl(testServerUrl() + "/obs-pumping")
<< QCoapMessage::Type::Confirmable
<< QtCoap::SecurityMode::NoSecurity;
}
void tst_QCoapClient::observe()
{
CHECK_FOR_COAP_SERVER;
QFETCH(QUrl, url);
QFETCH(QCoapMessage::Type, type);
QFETCH(QtCoap::SecurityMode, security);
QCoapClientForSecurityTests client(security);
if (client.securitySetupMissing())
QSKIP("Skipping this test, security is not configured properly");
QCoapRequest request(url);
request.setType(type);
QSharedPointer<QCoapReply> reply(client.observe(request),
&QObject::deleteLater);
QVERIFY(!reply.isNull());
QSignalSpy spyReplyNotified(reply.data(), &QCoapReply::notified);
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QTRY_COMPARE_WITH_TIMEOUT(spyReplyNotified.size(), 3, 30000);
client.cancelObserve(reply.data());
#ifdef QT_BUILD_INTERNAL
QCOMPARE(reply->url(),
QCoapRequestPrivate::adjustedUrl(url, client.isSecure()));
#endif
QCOMPARE(reply->request().method(), QtCoap::Method::Get);
QVERIFY2(!spyReplyNotified.wait(7000), "'Notify' signal received after cancelling observe");
QCOMPARE(spyReplyFinished.size(), 1);
for (QList<QVariant> receivedSignals : std::as_const(spyReplyNotified)) {
QRegularExpression regexp(QStringLiteral("..:..:.."));
QByteArray payload = receivedSignals.at(1).value<QCoapMessage>().payload();
QString error = QString("Invalid payload for 'notified' signal: %1").arg(QString(payload));
QVERIFY2(regexp.match(payload).hasMatch(), qPrintable(error));
}
}
void tst_QCoapClient::confirmableMulticast()
{
QCoapClient client;
const auto reply = client.get(QCoapRequest("224.0.1.187", QCoapMessage::Type::Confirmable));
QVERIFY2(!reply, "Confirmable multicast request didn't fail as expected.");
}
void tst_QCoapClient::multicast()
{
#ifdef QT_BUILD_INTERNAL
QCoapClientForMulticastTests client;
QCoapRequest request = QCoapRequest(QUrl("224.0.1.187"));
request.setToken("abc");
QCoapReply *reply = client.get(request);
QVERIFY(reply);
QHostAddress host0("10.20.30.40");
QHostAddress host1("10.20.30.41");
// Simulate sending unicast responses to the multicast request
emit client.connection()->readyRead("SE\xAD/abc\xC0\xFFReply0", host0);
emit client.connection()->readyRead("SE\xAD/abc\xC0\xFFReply1", host1);
QSignalSpy spyMulticastResponse(&client, &QCoapClient::responseToMulticastReceived);
QTRY_COMPARE(spyMulticastResponse.size(), 2);
QCoapMessage message0 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(0).at(1));
QCOMPARE(message0.payload(), "Reply0");
QHostAddress sender0 = qvariant_cast<QHostAddress>(spyMulticastResponse.at(0).at(2));
QCOMPARE(sender0, host0);
QCoapMessage message1 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(1).at(1));
QCOMPARE(message1.payload(), "Reply1");
QHostAddress sender1 = qvariant_cast<QHostAddress>(spyMulticastResponse.at(1).at(2));
QCOMPARE(sender1, host1);
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
void tst_QCoapClient::multicast_blockwise()
{
#ifdef QT_BUILD_INTERNAL
QCoapClientForMulticastTests client;
QCoapRequest request = QCoapRequest(QUrl("224.0.1.187"));
request.setToken("abc");
QCoapReply *reply = client.get(request);
QVERIFY(reply);
QHostAddress host0("10.20.30.40");
QHostAddress host1("10.20.30.41");
// Simulate blockwise transfer responses coming from two different hosts
emit client.connection()->readyRead("SE#}abc\xC0\xB1\x1D\xFFReply1", host0);
emit client.connection()->readyRead("SE#}abc\xC0\xB1\x1D\xFFReply3", host1);
emit client.connection()->readyRead("SE#~abc\xC0\xB1%\xFFReply2", host0);
emit client.connection()->readyRead("SE#~abc\xC0\xB1%\xFFReply4", host1);
QSignalSpy spyMulticastResponse(&client, &QCoapClient::responseToMulticastReceived);
QTRY_COMPARE(spyMulticastResponse.size(), 2);
QCoapMessage message0 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(0).at(1));
QCOMPARE(message0.payload(), "Reply1Reply2");
QHostAddress sender0 = qvariant_cast<QHostAddress>(spyMulticastResponse.at(0).at(2));
QCOMPARE(sender0, host0);
QCoapMessage message1 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(1).at(1));
QCOMPARE(message1.payload(), "Reply3Reply4");
QHostAddress sender1 = qvariant_cast<QHostAddress>(spyMulticastResponse.at(1).at(2));
QCOMPARE(sender1, host1);
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
void tst_QCoapClient::setMinimumTokenSize_data()
{
QTest::addColumn<int>("minTokenSize");
QTest::addColumn<int>("expectedMinSize");
QTest::newRow("in_range") << 6 << 6;
QTest::newRow("out_of_range_small") << 0 << 4;
QTest::newRow("out_of_range_big") << 9 << 4;
}
void noMessageOutput(QtMsgType, const QMessageLogContext &, const QString &) {}
void tst_QCoapClient::setMinimumTokenSize()
{
#ifdef QT_BUILD_INTERNAL
// Don't show warning messages for the out of range values
qInstallMessageHandler(noMessageOutput);
QFETCH(int, minTokenSize);
QFETCH(int, expectedMinSize);
const int maxSize = 8;
for (int i = 0; i < 20; ++i) {
QCoapClientForSocketErrorTests client;
client.setMinimumTokenSize(minTokenSize);
// With QCoapClientForSocketErrorTests the request will fail, but it doesn't matter,
// we are interested only in the generated token.
QSignalSpy spyClientError(&client, &QCoapClient::error);
QScopedPointer<QCoapReply> reply;
reply.reset(client.get(QCoapRequest("127.0.0.1")));
QTRY_COMPARE_WITH_TIMEOUT(spyClientError.size(), 1, 300);
QVERIFY(reply->request().tokenLength() >= expectedMinSize);
QVERIFY(reply->request().tokenLength() <= maxSize);
}
#else
QSKIP("Not an internal build, skipping this test");
#endif
}
QTEST_MAIN(tst_QCoapClient)
#include "tst_qcoapclient.moc"