mirror of https://github.com/nymea/nymea.git
1286 lines
57 KiB
C++
1286 lines
57 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* *
|
|
* Copyright (C) 2015 Simon Stürz <simon.stuerz@guh.io> *
|
|
* Copyright (C) 2014 Michael Zanetti <michael_zanetti@gmx.net> *
|
|
* *
|
|
* This file is part of nymea. *
|
|
* *
|
|
* nymea is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, version 2 of the License. *
|
|
* *
|
|
**
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with nymea. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "nymeatestbase.h"
|
|
#include "nymeacore.h"
|
|
#include "devicemanager.h"
|
|
#include "mocktcpserver.h"
|
|
#include "../../utils/pushbuttonagent.h"
|
|
|
|
#include <QtTest/QtTest>
|
|
#include <QCoreApplication>
|
|
#include <QTcpSocket>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkRequest>
|
|
#include <QNetworkReply>
|
|
#include <QCoreApplication>
|
|
#include <QMetaType>
|
|
|
|
using namespace nymeaserver;
|
|
|
|
class TestJSONRPC: public NymeaTestBase
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void initTestCase();
|
|
|
|
void testHandshake();
|
|
|
|
void testInitialSetup();
|
|
|
|
void testRevokeToken();
|
|
|
|
void testBasicCall_data();
|
|
void testBasicCall();
|
|
|
|
void introspect();
|
|
|
|
void enableDisableNotifications_data();
|
|
void enableDisableNotifications();
|
|
|
|
void deviceAddedRemovedNotifications();
|
|
void ruleAddedRemovedNotifications();
|
|
|
|
void ruleActiveChangedNotifications();
|
|
|
|
void deviceChangedNotifications();
|
|
|
|
void stateChangeEmitsNotifications();
|
|
|
|
void pluginConfigChangeEmitsNotification();
|
|
|
|
/*
|
|
Cases for push button auth:
|
|
|
|
Case 1: regular pushbutton
|
|
- alice sends JSONRPC.RequestPushButtonAuth, gets "OK" back (if push button hardware is available)
|
|
- alice pushes the hardware button and gets a notification on jsonrpc containing the token for local auth
|
|
*/
|
|
void testPushButtonAuth();
|
|
|
|
/*
|
|
Case 2: if we have an attacker in the network, he could try to call requestPushButtonAuth and
|
|
hope someone would eventually press the button and give him a token. In order to prevent this
|
|
any previous attempt for a push button auth needs to be cancelled when a new request comes in:
|
|
|
|
* Mallory does RequestPushButtonAuth, gets OK back
|
|
* Alice does RequestPushButtonAuth,
|
|
* Mallory receives a "PushButtonFailed" notification
|
|
* Alice receives OK
|
|
* alice presses the hardware button
|
|
* Alice reveices a notification with token, mallory receives nothing
|
|
|
|
Case 3: Mallory tries to hijack it back again
|
|
|
|
* Mallory does RequestPushButtonAuth, gets OK back
|
|
* Alice does RequestPusButtonAuth,
|
|
* Alice gets ok reply, Mallory gets failed notification
|
|
* Mallory quickly does RequestPushButtonAuth again to win the fight
|
|
* Alice gets failed notification and can instruct the user to _not_ press the button now until procedure is restarted
|
|
*/
|
|
void testPushButtonAuthInterrupt();
|
|
|
|
void testPushButtonAuthConnectionDrop();
|
|
|
|
void testInitialSetupWithPushButtonAuth();
|
|
|
|
void testDataFragmentation_data();
|
|
void testDataFragmentation();
|
|
|
|
void testGarbageData();
|
|
|
|
private:
|
|
QStringList extractRefs(const QVariant &variant);
|
|
|
|
};
|
|
|
|
QStringList TestJSONRPC::extractRefs(const QVariant &variant)
|
|
{
|
|
if (variant.canConvert(QVariant::String)) {
|
|
if (variant.toString().startsWith("$ref")) {
|
|
return QStringList() << variant.toString();
|
|
}
|
|
}
|
|
if (variant.canConvert(QVariant::List)) {
|
|
QStringList refs;
|
|
foreach (const QVariant tmp, variant.toList()) {
|
|
refs << extractRefs(tmp);
|
|
}
|
|
return refs;
|
|
}
|
|
if (variant.canConvert(QVariant::Map)) {
|
|
QStringList refs;
|
|
foreach (const QVariant tmp, variant.toMap()) {
|
|
refs << extractRefs(tmp);
|
|
}
|
|
return refs;
|
|
}
|
|
return QStringList();
|
|
}
|
|
|
|
void TestJSONRPC::initTestCase()
|
|
{
|
|
NymeaTestBase::initTestCase();
|
|
QLoggingCategory::setFilterRules("*.debug=false\n"
|
|
"JsonRpc*.debug=true");
|
|
}
|
|
|
|
void TestJSONRPC::testHandshake()
|
|
{
|
|
// first test if the handshake message is auto-sent upon connecting
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
QUuid newClientId = QUuid::createUuid();
|
|
m_mockTcpServer->clientConnected(newClientId);
|
|
QVERIFY2(spy.count() > 0, "Did not get the handshake message upon connect.");
|
|
QVERIFY2(spy.first().first() == newClientId, "Handshake message addressed at the wrong client.");
|
|
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
QVariantMap handShake = jsonDoc.toVariant().toMap();
|
|
QString nymeaVersionString(NYMEA_VERSION_STRING);
|
|
QVERIFY2(handShake.value("version").toString() == nymeaVersionString, "Handshake version doesn't match nymea version.");
|
|
|
|
// Check whether pushButtonAuth is disabled
|
|
QCOMPARE(handShake.value("pushButtonAuthAvailable").toBool(), false);
|
|
|
|
// Now register push button agent
|
|
PushButtonAgent pushButtonAgent;
|
|
pushButtonAgent.init();
|
|
|
|
// And now check if it is sent again when calling JSONRPC.Hello
|
|
handShake = injectAndWait("JSONRPC.Hello").toMap();
|
|
QCOMPARE(handShake.value("params").toMap().value("version").toString(), nymeaVersionString);
|
|
|
|
m_mockTcpServer->clientDisconnected(newClientId);
|
|
|
|
// Check whether pushButtonAuth is now
|
|
QCOMPARE(handShake.value("params").toMap().value("pushButtonAuthAvailable").toBool(), true);
|
|
|
|
// And now check if it is sent again when calling JSONRPC.Hello
|
|
handShake = injectAndWait("JSONRPC.Hello").toMap();
|
|
QCOMPARE(handShake.value("params").toMap().value("version").toString(), nymeaVersionString);
|
|
}
|
|
|
|
void TestJSONRPC::testInitialSetup()
|
|
{
|
|
foreach (const QString &user, NymeaCore::instance()->userManager()->users()) {
|
|
NymeaCore::instance()->userManager()->removeUser(user);
|
|
}
|
|
NymeaCore::instance()->userManager()->removeUser("");
|
|
|
|
QVERIFY(NymeaCore::instance()->userManager()->initRequired());
|
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
|
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
QVERIFY(spy.isValid());
|
|
|
|
// Introspect call should work in any case
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Introspect\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
QVariantMap response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling introspect on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
|
|
|
|
// Hello call should work in any case too
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Hello on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("initialSetupRequired").toBool(), true);
|
|
|
|
// Any other call should fail with "unauthorized" even if we use a previously valid token
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Version on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized"));
|
|
|
|
// Except CreateUser
|
|
|
|
// But it should still fail when giving a an invalid username
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling CreateUser on uninitialized instance with invalid user:" << response.value("status").toString() << response.value("params").toMap().value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
|
|
|
// or when giving a bad password
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"weak\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling CreateUser on uninitialized instance with weak password:" << response.value("status").toString() << response.value("params").toMap().value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
|
|
|
// Now lets play by the rules (with an uppercase email)
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling CreateUser on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 1);
|
|
|
|
// Now that we have a user, initialSetup should be false in the Hello call
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Hello on initialized instance:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("initialSetupRequired").toBool(), false);
|
|
|
|
// Calls should still fail, given we didn't get a new token yet
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Version with old token:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized"));
|
|
|
|
// Now lets authenticate with a wrong user
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@wrong.domain\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Authenticate with wrong user:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("success").toBool(), false);
|
|
QVERIFY(response.value("params").toMap().value("token").toByteArray().isEmpty());
|
|
|
|
|
|
// Now lets authenticate with a wrong password
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Authenticate with wrong password:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("success").toBool(), false);
|
|
QVERIFY(response.value("params").toMap().value("token").toByteArray().isEmpty());
|
|
|
|
|
|
// Now lets authenticate for real (but intentionally use a lowercase email here, should still work)
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Authenticate with valid credentials:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("success").toBool(), true);
|
|
m_apiToken = response.value("params").toMap().value("token").toByteArray();
|
|
QVERIFY(!m_apiToken.isEmpty());
|
|
|
|
// Now do a Version call with the valid token and it should work
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
|
|
}
|
|
|
|
void TestJSONRPC::testRevokeToken()
|
|
{
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
QVERIFY(spy.isValid());
|
|
|
|
// Now get all the tokens
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
QVariantMap response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Getting existing Tokens" << response.value("status").toString() << response;
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QVariantList tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
|
QCOMPARE(tokenList.count(), 1);
|
|
QUuid oldTokenId = tokenList.first().toMap().value("id").toUuid();
|
|
|
|
// Authenticate and create a new token
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Authenticate with valid credentials:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.value("params").toMap().value("success").toBool(), true);
|
|
QByteArray newToken = response.value("params").toMap().value("token").toByteArray();
|
|
QVERIFY(!newToken.isEmpty());
|
|
|
|
// Now do a Version call with the new token and it should work
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
|
|
// Now get all the tokens using the old token
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Tokens" << response.value("status").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
|
QCOMPARE(tokenList.count(), 2);
|
|
|
|
// find the new token
|
|
QUuid newTokenId;
|
|
foreach (const QVariant &tokenInfo, tokenList) {
|
|
if (tokenInfo.toMap().value("id").toUuid() != oldTokenId) {
|
|
newTokenId = tokenInfo.toMap().value("id").toUuid();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Revoke the new token
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling RemoveToken" << response.value("status").toString() << response;
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
|
|
// Do a call with the now removed token, it should be forbidden
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant().toMap();
|
|
qWarning() << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
|
QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized"));
|
|
}
|
|
|
|
void TestJSONRPC::testBasicCall_data()
|
|
{
|
|
QTest::addColumn<QByteArray>("call");
|
|
QTest::addColumn<bool>("idValid");
|
|
QTest::addColumn<bool>("valid");
|
|
|
|
QTest::newRow("valid call 1") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}") << true << true;
|
|
QTest::newRow("valid call 2") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n") << true << true;
|
|
QTest::newRow("valid call 3") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n\n\n\n") << true << true;
|
|
QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\"}\n") << false << false;
|
|
QTest::newRow("missing method") << QByteArray("{\"id\":42}\n") << true << false;
|
|
QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"JSO}\n") << false << false;
|
|
QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\"}\n") << true << false;
|
|
QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\"}\n") << true << false;
|
|
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\"}\n") << true << false;
|
|
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"params\":{\"törööö\":\"chooo-chooo\"}}\n") << true << false;
|
|
}
|
|
|
|
void TestJSONRPC::testBasicCall()
|
|
{
|
|
QFETCH(QByteArray, call);
|
|
QFETCH(bool, idValid);
|
|
QFETCH(bool, valid);
|
|
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
QVERIFY(spy.isValid());
|
|
|
|
m_mockTcpServer->injectData(m_clientId, call);
|
|
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
|
|
// Make sure we got exactly one response
|
|
QVERIFY(spy.count() == 1);
|
|
|
|
// Make sure the response goes to the correct clientId
|
|
QCOMPARE(spy.first().first().toString(), m_clientId.toString());
|
|
|
|
// Make sure the response it a valid JSON string
|
|
QJsonParseError error;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().last().toByteArray(), &error);
|
|
QCOMPARE(error.error, QJsonParseError::NoError);
|
|
|
|
// Make sure the response\"s id is the same as our command
|
|
if (idValid) {
|
|
QCOMPARE(jsonDoc.toVariant().toMap().value("id").toInt(), 42);
|
|
}
|
|
if (valid) {
|
|
QVERIFY2(jsonDoc.toVariant().toMap().value("status").toString() == "success", "Call wasn't parsed correctly by nymea.");
|
|
}
|
|
}
|
|
|
|
void TestJSONRPC::introspect()
|
|
{
|
|
QVariant response = injectAndWait("JSONRPC.Introspect");
|
|
QVariantMap methods = response.toMap().value("params").toMap().value("methods").toMap();
|
|
QVariantMap notifications = response.toMap().value("params").toMap().value("notifications").toMap();
|
|
QVariantMap types = response.toMap().value("params").toMap().value("types").toMap();
|
|
|
|
QVERIFY2(methods.count() > 0, "No methods in Introspect response!");
|
|
QVERIFY2(notifications.count() > 0, "No notifications in Introspect response!");
|
|
QVERIFY2(types.count() > 0, "No types in Introspect response!");
|
|
|
|
// Make sure all $ref: pointers have their according type defined
|
|
QVariantMap allItems = methods.unite(notifications).unite(types);
|
|
foreach (const QVariant &item, allItems) {
|
|
foreach (const QString &ref, extractRefs(item)) {
|
|
QString typeId = ref;
|
|
typeId.remove("$ref:");
|
|
QVERIFY2(types.contains(typeId), QString("Undefined ref: %1. Did you forget to add it to JsonTypes::allTypes()?").arg(ref).toLatin1().data());
|
|
}
|
|
}
|
|
}
|
|
|
|
void TestJSONRPC::enableDisableNotifications_data()
|
|
{
|
|
QTest::addColumn<QString>("enabled");
|
|
|
|
QTest::newRow("enabled") << "true";
|
|
QTest::newRow("disabled") << "false";
|
|
}
|
|
|
|
void TestJSONRPC::enableDisableNotifications()
|
|
{
|
|
QFETCH(QString, enabled);
|
|
|
|
QVariantMap params;
|
|
params.insert("enabled", enabled);
|
|
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
|
|
|
|
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toString(), enabled);
|
|
}
|
|
|
|
void TestJSONRPC::deviceAddedRemovedNotifications()
|
|
{
|
|
// enable notificartions
|
|
QCOMPARE(enableNotifications(), true);
|
|
|
|
// Setup connection to mock client
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// add device and wait for notification
|
|
QVariantList deviceParams;
|
|
QVariantMap httpportParam;
|
|
httpportParam.insert("paramTypeId", httpportParamTypeId);
|
|
httpportParam.insert("value", 8765);
|
|
deviceParams.append(httpportParam);
|
|
|
|
QVariantMap params; clientSpy.clear();
|
|
params.insert("deviceClassId", mockDeviceClassId);
|
|
params.insert("name", "Mock device");
|
|
params.insert("deviceParams", deviceParams);
|
|
QVariant response = injectAndWait("Devices.AddConfiguredDevice", params);
|
|
clientSpy.wait(2000);
|
|
verifyDeviceError(response);
|
|
QVariantMap notificationDeviceMap = checkNotification(clientSpy, "Devices.DeviceAdded").toMap().value("params").toMap().value("device").toMap();
|
|
|
|
DeviceId deviceId = DeviceId(response.toMap().value("params").toMap().value("deviceId").toString());
|
|
QVERIFY(!deviceId.isNull());
|
|
|
|
// check the DeviceAdded notification
|
|
QCOMPARE(notificationDeviceMap.value("deviceClassId").toString(), mockDeviceClassId.toString());
|
|
QCOMPARE(notificationDeviceMap.value("id").toString(), deviceId.toString());
|
|
foreach (const QVariant ¶m, notificationDeviceMap.value("params").toList()) {
|
|
if (param.toMap().value("name").toString() == "httpport") {
|
|
QCOMPARE(param.toMap().value("value").toInt(), httpportParam.value("value").toInt());
|
|
}
|
|
}
|
|
|
|
// now remove the device and check the device removed notification
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("deviceId", deviceId);
|
|
response = injectAndWait("Devices.RemoveConfiguredDevice", params);
|
|
clientSpy.wait(2000);
|
|
verifyDeviceError(response);
|
|
checkNotification(clientSpy, "Devices.DeviceRemoved");
|
|
|
|
QCOMPARE(disableNotifications(), true);
|
|
}
|
|
|
|
void TestJSONRPC::ruleAddedRemovedNotifications()
|
|
{
|
|
// enable notificartions
|
|
QCOMPARE(enableNotifications(), true);
|
|
|
|
// Setup connection to mock client
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// Add rule and wait for notification
|
|
// StateDescriptor
|
|
QVariantMap stateDescriptor;
|
|
stateDescriptor.insert("stateTypeId", mockIntStateId);
|
|
stateDescriptor.insert("deviceId", m_mockDeviceId);
|
|
stateDescriptor.insert("operator", JsonTypes::valueOperatorToString(Types::ValueOperatorLess));
|
|
stateDescriptor.insert("value", "20");
|
|
|
|
QVariantMap stateEvaluator;
|
|
stateEvaluator.insert("stateDescriptor", stateDescriptor);
|
|
|
|
// RuleAction
|
|
QVariantMap actionNoParams;
|
|
actionNoParams.insert("actionTypeId", mockActionIdNoParams);
|
|
actionNoParams.insert("deviceId", m_mockDeviceId);
|
|
actionNoParams.insert("ruleActionParams", QVariantList());
|
|
|
|
// EventDescriptor
|
|
QVariantMap eventDescriptor;
|
|
eventDescriptor.insert("eventTypeId", mockEvent1Id);
|
|
eventDescriptor.insert("deviceId", m_mockDeviceId);
|
|
eventDescriptor.insert("paramDescriptors", QVariantList());
|
|
|
|
QVariantMap params;
|
|
params.insert("name", "Test Rule notifications");
|
|
params.insert("actions", QVariantList() << actionNoParams);
|
|
params.insert("eventDescriptors", QVariantList() << eventDescriptor);
|
|
params.insert("stateEvaluator", stateEvaluator);
|
|
|
|
QVariant response = injectAndWait("Rules.AddRule", params);
|
|
clientSpy.wait(2000);
|
|
QVariantMap notificationRuleMap = checkNotification(clientSpy, "Rules.RuleAdded").toMap().value("params").toMap().value("rule").toMap();
|
|
verifyRuleError(response);
|
|
|
|
RuleId ruleId = RuleId(response.toMap().value("params").toMap().value("ruleId").toString());
|
|
QVERIFY(!ruleId.isNull());
|
|
|
|
QCOMPARE(notificationRuleMap.value("enabled").toBool(), true);
|
|
QCOMPARE(notificationRuleMap.value("name").toString(), params.value("name").toString());
|
|
QCOMPARE(notificationRuleMap.value("id").toString(), ruleId.toString());
|
|
QCOMPARE(notificationRuleMap.value("actions").toList(), QVariantList() << actionNoParams);
|
|
QCOMPARE(notificationRuleMap.value("stateEvaluator").toMap().value("stateDescriptor").toMap(), stateDescriptor);
|
|
QCOMPARE(notificationRuleMap.value("eventDescriptors").toList(), QVariantList() << eventDescriptor);
|
|
QCOMPARE(notificationRuleMap.value("exitActions").toList(), QVariantList());
|
|
|
|
// now remove the rule and check the RuleRemoved notification
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("ruleId", ruleId);
|
|
response = injectAndWait("Rules.RemoveRule", params);
|
|
clientSpy.wait(2000);
|
|
checkNotification(clientSpy, "Devices.DeviceRemoved");
|
|
verifyRuleError(response);
|
|
|
|
QCOMPARE(disableNotifications(), true);
|
|
}
|
|
|
|
void TestJSONRPC::ruleActiveChangedNotifications()
|
|
{
|
|
// enable notificartions
|
|
QVariantMap params;
|
|
params.insert("enabled", true);
|
|
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), true);
|
|
|
|
// Add rule and wait for notification
|
|
// StateDescriptor
|
|
QVariantMap stateDescriptor;
|
|
stateDescriptor.insert("stateTypeId", mockIntStateId);
|
|
stateDescriptor.insert("deviceId", m_mockDeviceId);
|
|
stateDescriptor.insert("operator", JsonTypes::valueOperatorToString(Types::ValueOperatorEquals));
|
|
stateDescriptor.insert("value", "20");
|
|
|
|
QVariantMap stateEvaluator;
|
|
stateEvaluator.insert("stateDescriptor", stateDescriptor);
|
|
|
|
// RuleAction
|
|
QVariantMap actionNoParams;
|
|
actionNoParams.insert("actionTypeId", mockActionIdNoParams);
|
|
actionNoParams.insert("deviceId", m_mockDeviceId);
|
|
actionNoParams.insert("ruleActionParams", QVariantList());
|
|
|
|
params.clear(); response.clear();
|
|
params.insert("name", "Test Rule notifications");
|
|
params.insert("actions", QVariantList() << actionNoParams);
|
|
params.insert("stateEvaluator", stateEvaluator);
|
|
|
|
// Setup connection to mock client
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
response = injectAndWait("Rules.AddRule", params);
|
|
clientSpy.wait();
|
|
QVariant notificationVariant = checkNotification(clientSpy, "Rules.RuleAdded");
|
|
verifyRuleError(response);
|
|
|
|
QVariantMap notificationRuleMap = notificationVariant.toMap().value("params").toMap().value("rule").toMap();
|
|
RuleId ruleId = RuleId(notificationRuleMap.value("id").toString());
|
|
QVERIFY(!ruleId.isNull());
|
|
QCOMPARE(notificationRuleMap.value("enabled").toBool(), true);
|
|
QCOMPARE(notificationRuleMap.value("name").toString(), params.value("name").toString());
|
|
QCOMPARE(notificationRuleMap.value("id").toString(), ruleId.toString());
|
|
QCOMPARE(notificationRuleMap.value("actions").toList(), QVariantList() << actionNoParams);
|
|
QCOMPARE(notificationRuleMap.value("stateEvaluator").toMap().value("stateDescriptor").toMap(), stateDescriptor);
|
|
QCOMPARE(notificationRuleMap.value("exitActions").toList(), QVariantList());
|
|
|
|
// set the rule active
|
|
QNetworkAccessManager nam;
|
|
QSignalSpy spy(&nam, SIGNAL(finished(QNetworkReply*)));
|
|
|
|
// state state to 20
|
|
qDebug() << "setting mock int state to 20";
|
|
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(mockIntStateId.toString()).arg(20)));
|
|
QNetworkReply *reply = nam.get(request);
|
|
connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
|
|
|
|
spy.wait();
|
|
notificationVariant = checkNotification(clientSpy, "Rules.RuleActiveChanged");
|
|
verifyRuleError(response);
|
|
|
|
QCOMPARE(notificationVariant.toMap().value("params").toMap().value("ruleId").toString(), ruleId.toString());
|
|
QCOMPARE(notificationVariant.toMap().value("params").toMap().value("active").toBool(), true);
|
|
|
|
spy.clear(); clientSpy.clear();
|
|
|
|
// set the rule inactive
|
|
qDebug() << "setting mock int state to 42";
|
|
QNetworkRequest request2(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(mockIntStateId.toString()).arg(42)));
|
|
QNetworkReply *reply2 = nam.get(request2);
|
|
spy.wait();
|
|
QCOMPARE(spy.count(), 1);
|
|
connect(reply2, SIGNAL(finished()), reply2, SLOT(deleteLater()));
|
|
|
|
|
|
clientSpy.wait();
|
|
notificationVariant = checkNotification(clientSpy, "Rules.RuleActiveChanged");
|
|
verifyRuleError(response);
|
|
|
|
QCOMPARE(notificationVariant.toMap().value("params").toMap().value("ruleId").toString(), ruleId.toString());
|
|
QCOMPARE(notificationVariant.toMap().value("params").toMap().value("active").toBool(), false);
|
|
|
|
// now remove the rule and check the RuleRemoved notification
|
|
params.clear(); response.clear();
|
|
params.insert("ruleId", ruleId);
|
|
response = injectAndWait("Rules.RemoveRule", params);
|
|
|
|
clientSpy.wait();
|
|
notificationVariant = checkNotification(clientSpy, "Rules.RuleRemoved");
|
|
checkNotification(clientSpy, "Logging.LogDatabaseUpdated");
|
|
verifyRuleError(response);
|
|
|
|
QCOMPARE(notificationVariant.toMap().value("params").toMap().value("ruleId").toString(), ruleId.toString());
|
|
}
|
|
|
|
void TestJSONRPC::deviceChangedNotifications()
|
|
{
|
|
// enable notificartions
|
|
QVariantMap params;
|
|
params.insert("enabled", true);
|
|
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), true);
|
|
|
|
// Setup connection to mock client
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// ADD
|
|
// add device and wait for notification
|
|
QVariantList deviceParams;
|
|
QVariantMap httpportParam;
|
|
httpportParam.insert("paramTypeId", httpportParamTypeId);
|
|
httpportParam.insert("value", 23234);
|
|
deviceParams.append(httpportParam);
|
|
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("deviceClassId", mockDeviceClassId);
|
|
params.insert("name", "Mock");
|
|
params.insert("deviceParams", deviceParams);
|
|
response = injectAndWait("Devices.AddConfiguredDevice", params);
|
|
DeviceId deviceId = DeviceId(response.toMap().value("params").toMap().value("deviceId").toString());
|
|
QVERIFY(!deviceId.isNull());
|
|
clientSpy.wait();
|
|
verifyDeviceError(response);
|
|
QVariantMap notificationDeviceMap = checkNotification(clientSpy, "Devices.DeviceAdded").toMap().value("params").toMap().value("device").toMap();
|
|
|
|
QCOMPARE(notificationDeviceMap.value("deviceClassId").toString(), mockDeviceClassId.toString());
|
|
QCOMPARE(notificationDeviceMap.value("id").toString(), deviceId.toString());
|
|
foreach (const QVariant ¶m, notificationDeviceMap.value("params").toList()) {
|
|
if (param.toMap().value("name").toString() == "httpport") {
|
|
QCOMPARE(param.toMap().value("value").toInt(), httpportParam.value("value").toInt());
|
|
}
|
|
}
|
|
|
|
// RECONFIGURE
|
|
// now reconfigure the device and check the deviceChanged notification
|
|
QVariantList newDeviceParams;
|
|
QVariantMap newHttpportParam;
|
|
newHttpportParam.insert("paramTypeId", httpportParamTypeId);
|
|
newHttpportParam.insert("value", 45473);
|
|
newDeviceParams.append(newHttpportParam);
|
|
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("deviceId", deviceId);
|
|
params.insert("deviceParams", newDeviceParams);
|
|
response = injectAndWait("Devices.ReconfigureDevice", params);
|
|
clientSpy.wait(2000);
|
|
verifyDeviceError(response);
|
|
QVariantMap reconfigureDeviceNotificationMap = checkNotification(clientSpy, "Devices.DeviceChanged").toMap().value("params").toMap().value("device").toMap();
|
|
QCOMPARE(reconfigureDeviceNotificationMap.value("deviceClassId").toString(), mockDeviceClassId.toString());
|
|
QCOMPARE(reconfigureDeviceNotificationMap.value("id").toString(), deviceId.toString());
|
|
foreach (const QVariant ¶m, reconfigureDeviceNotificationMap.value("params").toList()) {
|
|
if (param.toMap().value("name").toString() == "httpport") {
|
|
QCOMPARE(param.toMap().value("value").toInt(), newHttpportParam.value("value").toInt());
|
|
}
|
|
}
|
|
|
|
// EDIT device name
|
|
QString deviceName = "Test device 1234";
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("deviceId", deviceId);
|
|
params.insert("name", deviceName);
|
|
response = injectAndWait("Devices.EditDevice", params);
|
|
clientSpy.wait(2000);
|
|
verifyDeviceError(response);
|
|
QVariantMap editDeviceNotificationMap = checkNotification(clientSpy, "Devices.DeviceChanged").toMap().value("params").toMap().value("device").toMap();
|
|
QCOMPARE(editDeviceNotificationMap.value("deviceClassId").toString(), mockDeviceClassId.toString());
|
|
QCOMPARE(editDeviceNotificationMap.value("id").toString(), deviceId.toString());
|
|
QCOMPARE(editDeviceNotificationMap.value("name").toString(), deviceName);
|
|
|
|
// REMOVE
|
|
// now remove the device and check the device removed notification
|
|
params.clear(); response.clear(); clientSpy.clear();
|
|
params.insert("deviceId", deviceId);
|
|
response = injectAndWait("Devices.RemoveConfiguredDevice", params);
|
|
clientSpy.wait();
|
|
verifyDeviceError(response);
|
|
checkNotification(clientSpy, "Devices.DeviceRemoved");
|
|
checkNotification(clientSpy, "Logging.LogDatabaseUpdated");
|
|
}
|
|
|
|
void TestJSONRPC::stateChangeEmitsNotifications()
|
|
{
|
|
QCOMPARE(enableNotifications(), true);
|
|
bool found = false;
|
|
|
|
// Setup connection to mock client
|
|
QNetworkAccessManager nam;
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// trigger state change in mock device
|
|
int newVal = 38;
|
|
QUuid stateTypeId("80baec19-54de-4948-ac46-31eabfaceb83");
|
|
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(stateTypeId.toString()).arg(QString::number(newVal))));
|
|
QNetworkReply *reply = nam.get(request);
|
|
connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
|
|
QSignalSpy replySpy(reply, SIGNAL(finished()));
|
|
replySpy.wait();
|
|
|
|
// Make sure the notification contains all the stuff we expect
|
|
QVariantList stateChangedVariants = checkNotifications(clientSpy, "Devices.StateChanged");
|
|
QVERIFY2(!stateChangedVariants.isEmpty(), "Did not get Devices.StateChanged notification.");
|
|
qDebug() << "got" << stateChangedVariants.count() << "Devices.StateChanged notifications";
|
|
foreach (const QVariant &stateChangedVariant, stateChangedVariants) {
|
|
if (stateChangedVariant.toMap().value("params").toMap().value("stateTypeId").toUuid() == stateTypeId) {
|
|
found = true;
|
|
QCOMPARE(stateChangedVariant.toMap().value("params").toMap().value("value").toInt(), newVal);
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
qDebug() << QJsonDocument::fromVariant(stateChangedVariants).toJson();
|
|
|
|
QVERIFY2(found, "Could not find the correct Devices.StateChanged notification");
|
|
|
|
|
|
// Make sure the logg notification contains all the stuff we expect
|
|
QVariantList loggEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
|
|
QVERIFY2(!loggEntryAddedVariants.isEmpty(), "Did not get Logging.LogEntryAdded notification.");
|
|
qDebug() << "got" << loggEntryAddedVariants.count() << "Logging.LogEntryAdded notifications";
|
|
found = false;
|
|
foreach (const QVariant &loggEntryAddedVariant, loggEntryAddedVariants) {
|
|
if (loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("typeId").toUuid() == stateTypeId) {
|
|
found = true;
|
|
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("source").toString(), QString("LoggingSourceStates"));
|
|
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("value").toInt(), newVal);
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
qDebug() << QJsonDocument::fromVariant(loggEntryAddedVariants).toJson();
|
|
|
|
QVERIFY2(found, "Could not find the corresponding Logging.LogEntryAdded notification");
|
|
|
|
|
|
// Make sure the notification contains all the stuff we expect
|
|
QVariantList eventTriggeredVariants = checkNotifications(clientSpy, "Events.EventTriggered");
|
|
QVERIFY2(!eventTriggeredVariants.isEmpty(), "Did not get Events.EventTriggered notification.");
|
|
found = false;
|
|
qDebug() << "got" << eventTriggeredVariants.count() << "Events.EventTriggered notifications";
|
|
foreach (const QVariant &eventTriggeredVariant, eventTriggeredVariants) {
|
|
if (eventTriggeredVariant.toMap().value("params").toMap().value("event").toMap().value("eventTypeId").toUuid() == stateTypeId) {
|
|
found = true;
|
|
QCOMPARE(eventTriggeredVariant.toMap().value("params").toMap().value("event").toMap().value("params").toList().first().toMap().value("value").toInt(), newVal);
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
qDebug() << QJsonDocument::fromVariant(eventTriggeredVariants).toJson();
|
|
|
|
QVERIFY2(found, "Could not find the corresponding Events.EventTriggered notification");
|
|
|
|
// Now turn off notifications
|
|
QCOMPARE(disableNotifications(), true);
|
|
|
|
// Fire the a statechange once again
|
|
clientSpy.clear();
|
|
newVal = 42;
|
|
request.setUrl(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(stateTypeId.toString()).arg(newVal)));
|
|
reply = nam.get(request);
|
|
connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
|
|
|
|
// Lets wait a max of 500ms for notifications
|
|
clientSpy.wait(500);
|
|
// but make sure it doesn't come
|
|
QCOMPARE(clientSpy.count(), 0);
|
|
|
|
// Now check that the state has indeed changed even though we didn't get a notification
|
|
QVariantMap params;
|
|
params.insert("deviceId", m_mockDeviceId);
|
|
params.insert("stateTypeId", stateTypeId);
|
|
QVariant response = injectAndWait("Devices.GetStateValue", params);
|
|
|
|
QCOMPARE(response.toMap().value("params").toMap().value("value").toInt(), newVal);
|
|
}
|
|
|
|
void TestJSONRPC::pluginConfigChangeEmitsNotification()
|
|
{
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
QVariantMap params;
|
|
params.insert("enabled", true);
|
|
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), true);
|
|
|
|
params.clear();
|
|
params.insert("pluginId", mockPluginId);
|
|
QVariantList pluginParams;
|
|
QVariantMap param1;
|
|
param1.insert("paramTypeId", configParamIntParamTypeId);
|
|
param1.insert("value", 42);
|
|
pluginParams.append(param1);
|
|
params.insert("configuration", pluginParams);
|
|
|
|
response = injectAndWait("Devices.SetPluginConfiguration", params);
|
|
|
|
QVariantList notificationData = checkNotifications(clientSpy, "Devices.PluginConfigurationChanged");
|
|
QCOMPARE(notificationData.first().toMap().value("notification").toString() == "Devices.PluginConfigurationChanged", true);
|
|
}
|
|
|
|
void TestJSONRPC::testPushButtonAuth()
|
|
{
|
|
PushButtonAgent pushButtonAgent;
|
|
pushButtonAgent.init();
|
|
|
|
QVariantMap params;
|
|
params.insert("deviceName", "pbtestdevice");
|
|
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
// Setup connection to mock client
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
pushButtonAgent.sendButtonPressed();
|
|
|
|
if (clientSpy.count() == 0) {
|
|
clientSpy.wait();
|
|
}
|
|
QVariantMap rsp = checkNotification(clientSpy, "JSONRPC.PushButtonAuthFinished").toMap();
|
|
|
|
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
|
|
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
|
|
}
|
|
|
|
|
|
|
|
void TestJSONRPC::testPushButtonAuthInterrupt()
|
|
{
|
|
PushButtonAgent pushButtonAgent;
|
|
pushButtonAgent.init();
|
|
|
|
// m_clientId is registered in gutTestbase already, just using it here to improve readability of the test
|
|
QUuid aliceId = m_clientId;
|
|
|
|
// Create a new clientId for mallory and connect it to the server
|
|
QUuid malloryId = QUuid::createUuid();
|
|
m_mockTcpServer->clientConnected(malloryId);
|
|
|
|
// Snoop in on everything the TCP server sends to its clients.
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// request push button auth for client 1 (alice) and check for OK reply
|
|
QVariantMap params;
|
|
params.insert("deviceName", "alice");
|
|
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId1 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
|
|
// Request push button auth for client 2 (mallory)
|
|
clientSpy.clear();
|
|
params.clear();
|
|
params.insert("deviceName", "mallory");
|
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
// Both clients should receive something. Wait for it
|
|
if (clientSpy.count() < 2) {
|
|
clientSpy.wait();
|
|
}
|
|
|
|
// spy.at(0) should be the failed notification for alice
|
|
// spy.at(1) shpuld be the OK reply for mallory
|
|
|
|
|
|
// alice should have received a failed notification. She knows something's wrong.
|
|
QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.first().first().toUuid(), aliceId);
|
|
QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished"));
|
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId1);
|
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), false);
|
|
|
|
// Mallory instead should have received an OK
|
|
QVariantMap reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.at(1).first().toUuid(), malloryId);
|
|
QCOMPARE(reply.value("params").toMap().value("success").toBool(), true);
|
|
|
|
|
|
// Alice tries once more
|
|
clientSpy.clear();
|
|
params.clear();
|
|
params.insert("deviceName", "alice");
|
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId3 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
// Both clients should receive something. Wait for it
|
|
if (clientSpy.count() < 2) {
|
|
clientSpy.wait();
|
|
}
|
|
|
|
// spy.at(0) should be the failed notification for mallory
|
|
// spy.at(1) shpuld be the OK reply for alice
|
|
|
|
// mallory should have received a failed notification. She knows something's wrong.
|
|
notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.first().first().toUuid(), malloryId);
|
|
QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished"));
|
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId2);
|
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), false);
|
|
|
|
// Alice instead should have received an OK
|
|
reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.at(1).first().toUuid(), aliceId);
|
|
QCOMPARE(reply.value("params").toMap().value("success").toBool(), true);
|
|
|
|
clientSpy.clear();
|
|
|
|
// do the button press
|
|
pushButtonAgent.sendButtonPressed();
|
|
|
|
// Wait for things to happen
|
|
if (clientSpy.count() == 0) {
|
|
clientSpy.wait();
|
|
}
|
|
|
|
// There should have been only exactly one message sent, the token for alice
|
|
// Mallory should not have received anything
|
|
QCOMPARE(clientSpy.count(), 1);
|
|
notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.first().first().toUuid(), aliceId);
|
|
QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished"));
|
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3);
|
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
|
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
|
}
|
|
|
|
void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
|
{
|
|
PushButtonAgent pushButtonAgent;
|
|
pushButtonAgent.init();
|
|
|
|
// Snoop in on everything the TCP server sends to its clients.
|
|
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
// Create a new clientId for alice and connect it to the server
|
|
QUuid aliceId = QUuid::createUuid();
|
|
m_mockTcpServer->clientConnected(aliceId);
|
|
|
|
// request push button auth for client 1 (alice) and check for OK reply
|
|
QVariantMap params;
|
|
params.insert("deviceName", "alice");
|
|
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
|
|
// Disconnect alice
|
|
m_mockTcpServer->clientDisconnected(aliceId);
|
|
|
|
// Now try with bob
|
|
// Create a new clientId for bob and connect it to the server
|
|
QUuid bobId = QUuid::createUuid();
|
|
m_mockTcpServer->clientConnected(bobId);
|
|
|
|
// request push button auth for client 2 (bob) and check for OK reply
|
|
params.clear();
|
|
params.insert("deviceName", "bob");
|
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
clientSpy.clear();
|
|
|
|
pushButtonAgent.sendButtonPressed();
|
|
|
|
// Wait for things to happen
|
|
if (clientSpy.count() == 0) {
|
|
clientSpy.wait();
|
|
}
|
|
|
|
// There should have been only exactly one message sent, the token for bob
|
|
QCOMPARE(clientSpy.count(), 1);
|
|
QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(clientSpy.first().first().toUuid(), bobId);
|
|
QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished"));
|
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId);
|
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
|
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
|
|
|
}
|
|
|
|
void TestJSONRPC::testInitialSetupWithPushButtonAuth()
|
|
{
|
|
foreach (const QString &user, NymeaCore::instance()->userManager()->users()) {
|
|
NymeaCore::instance()->userManager()->removeUser(user);
|
|
}
|
|
NymeaCore::instance()->userManager()->removeUser("");
|
|
QVERIFY(NymeaCore::instance()->userManager()->initRequired());
|
|
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
QVERIFY(spy.isValid());
|
|
|
|
PushButtonAgent pushButtonAgent;
|
|
pushButtonAgent.init();
|
|
|
|
// Hello call should work in any case, telling us initial setup is required
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
QVariant response = jsonDoc.toVariant();
|
|
qWarning() << "Calling Hello on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString();
|
|
QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.toMap().value("params").toMap().value("initialSetupRequired").toBool(), true);
|
|
|
|
// request push button auth for alice and check for OK reply
|
|
QUuid aliceId = QUuid::createUuid();
|
|
m_mockTcpServer->clientConnected(aliceId);
|
|
|
|
QVariantMap params;
|
|
params.insert("deviceName", "alice");
|
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
|
|
|
spy.clear();
|
|
pushButtonAgent.sendButtonPressed();
|
|
|
|
// Wait for things to happen
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
|
|
// There should have been only exactly one message sent, the token for alice
|
|
QCOMPARE(spy.count(), 1);
|
|
QVariantMap notification = QJsonDocument::fromJson(spy.first().at(1).toByteArray()).toVariant().toMap();
|
|
QCOMPARE(spy.first().first().toUuid(), aliceId);
|
|
QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished"));
|
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId);
|
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
|
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
|
|
|
// initialSetupRequired should be false in Hello call now
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant();
|
|
qWarning() << "Calling Hello on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString();
|
|
QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("success"));
|
|
QCOMPARE(response.toMap().value("params").toMap().value("initialSetupRequired").toBool(), false);
|
|
|
|
|
|
// CreateUser without a token should fail now even though there are 0 users in the DB
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n");
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QVERIFY(spy.count() == 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
response = jsonDoc.toVariant();
|
|
qWarning() << "Calling CreateUser on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString();
|
|
QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("unauthorized"));
|
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
|
|
|
}
|
|
|
|
void TestJSONRPC::testDataFragmentation_data()
|
|
{
|
|
QTest::addColumn<QList<QByteArray> >("packets");
|
|
|
|
QList<QByteArray> packets;
|
|
|
|
packets.append("{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
|
QTest::newRow("1 packet") << packets;
|
|
|
|
packets.clear();
|
|
packets.append("{\"id\": 555, \"m");
|
|
packets.append("ethod\": \"JSONRPC.Hello\"}\n");
|
|
QTest::newRow("2 packets") << packets;
|
|
|
|
packets.clear();
|
|
packets.append("{\"id\": 555, \"m");
|
|
packets.append("ethod\": \"JSONRP");
|
|
packets.append("C.Hello\"}\n");
|
|
QTest::newRow("3 packets") << packets;
|
|
|
|
packets.clear();
|
|
packets.append("{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n{\"id\": 5556, \"metho");
|
|
QTest::newRow("next packet start appended") << packets;
|
|
}
|
|
|
|
void TestJSONRPC::testDataFragmentation()
|
|
{
|
|
QJsonDocument jsonDoc;
|
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
|
|
|
QFETCH(QList<QByteArray>, packets);
|
|
|
|
foreach (const QByteArray &packet, packets) {
|
|
spy.clear();
|
|
m_mockTcpServer->injectData(m_clientId, packet);
|
|
}
|
|
if (spy.count() == 0) {
|
|
spy.wait();
|
|
}
|
|
QCOMPARE(spy.count(), 1);
|
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
QCOMPARE(jsonDoc.toVariant().toMap().value("status").toString(), QStringLiteral("success"));
|
|
}
|
|
|
|
void TestJSONRPC::testGarbageData()
|
|
{
|
|
QSignalSpy spy(m_mockTcpServer, &MockTcpServer::connectionTerminated);
|
|
|
|
QByteArray data;
|
|
for (int i = 0; i < 1024; i++) {
|
|
data.append("a");
|
|
}
|
|
for (int i = 0; i < 11; i ++) {
|
|
m_mockTcpServer->injectData(m_clientId, data);
|
|
}
|
|
|
|
QCOMPARE(spy.count(), 1);
|
|
|
|
}
|
|
|
|
#include "testjsonrpc.moc"
|
|
|
|
QTEST_MAIN(TestJSONRPC)
|