/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2015 Simon Stürz * * Copyright (C) 2014 Michael Zanetti * * * * 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "nymeatestbase.h" #include "../../utils/pushbuttonagent.h" #include "nymeacore.h" #include "servers/mocktcpserver.h" using namespace nymeaserver; class TestJSONRPC: public NymeaTestBase { Q_OBJECT private slots: void initTestCase(); void testHandshake(); void testHandshakeLocale(); 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" // "JsonRpcTraffic.debug=true\n" "JsonRpc.debug=true\n" "Translations.debug=true\n" "Tests.debug=true"); } void TestJSONRPC::testHandshake() { QUuid newClientId = QUuid::createUuid(); m_mockTcpServer->clientConnected(newClientId); qApp->processEvents(); // Check the Hello reply QVariantMap handShake = injectAndWait("JSONRPC.Hello", QVariantMap(), newClientId).toMap(); QString nymeaVersionString(NYMEA_VERSION_STRING); QVERIFY2(handShake.value("params").toMap().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); // Check whether pushButtonAuth is now enabled QCOMPARE(handShake.value("params").toMap().value("pushButtonAuthAvailable").toBool(), true); emit m_mockTcpServer->clientDisconnected(newClientId); // 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::testHandshakeLocale() { // first test if the handshake message is auto-sent upon connecting QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); // Test withouth locale data QVariantMap handShake = injectAndWait("JSONRPC.Hello").toMap(); QCOMPARE(handShake.value("params").toMap().value("locale").toString(), QString("en_US")); QVariantMap supportedDevices = injectAndWait("Devices.GetSupportedDevices").toMap(); bool found = false; foreach (const QVariant &dcMap, supportedDevices.value("params").toMap().value("deviceClasses").toList()) { if (dcMap.toMap().value("id").toUuid() == mockDeviceAutoDeviceClassId) { QCOMPARE(dcMap.toMap().value("displayName").toString(), QString("Mock Device (Auto created)")); found = true; } } QVERIFY(found); // And now with locale info QVariantMap params; params.insert("locale", "de_DE"); handShake = injectAndWait("JSONRPC.Hello", params).toMap(); QCOMPARE(handShake.value("params").toMap().value("locale").toString(), QString("de_DE")); supportedDevices = injectAndWait("Devices.GetSupportedDevices").toMap(); found = false; foreach (const QVariant &dcMap, supportedDevices.value("params").toMap().value("deviceClasses").toList()) { if (dcMap.toMap().value("id").toUuid() == mockDeviceAutoDeviceClassId) { QCOMPARE(dcMap.toMap().value("displayName").toString(), QString("Mock Gerät (Automatisch erzeugt)")); found = true; } } QVERIFY(found); } 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()); QSignalSpy connectedSpy(m_mockTcpServer, &MockTcpServer::clientConnected); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); // Introspect call should work in any case qCDebug(dcTests()) << "Calling Introspect, expecting success"; 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(); qCDebug(dcTests()) << "Result:" << 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(); qCDebug(dcTests()) << "Calling Hello, expecting success"; 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(); qCDebug(dcTests()) << "Result:" << 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(); disconnectedSpy.clear(); qCDebug(dcTests()) << "Calling Version, expecting failure (unauthenticated)"; 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(); qCDebug(dcTests()) << "Result:" << response.value("status").toString() << response.value("error").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized")); // Connection should terminate if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); qCDebug(dcTests()) << "Mock client disconnected"; connectedSpy.clear(); emit m_mockTcpServer->clientConnected(m_clientId); if (connectedSpy.count() == 0) connectedSpy.wait(); QCOMPARE(connectedSpy.count(), 1); qCDebug(dcTests()) << "Mock client connected"; spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (spy.count() == 0) { spy.wait(); } QVERIFY(spy.count() == 1); // Except CreateUser // But it should still fail when giving a an invalid username spy.clear(); qCDebug(dcTests()) << "Calling CreateUser, expecting failure (bad username)"; 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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "Calling CreateUser, expecting failure (bad password)"; 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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "Calling CreateUser, expecting success"; 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(); qCDebug(dcTests) << "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(); qCDebug(dcTests()) << "Calling Hello, expecting success"; 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(); qCDebug(dcTests) << "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(); disconnectedSpy.clear(); qCDebug(dcTests()) << "Calling Version, expecting failure (bad token)"; 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(); qCDebug(dcTests) << "Calling Version with old token:" << response.value("status").toString() << response.value("error").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized")); // Connection should terminate if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); qCDebug(dcTests()) << "Mock client disconnected"; connectedSpy.clear(); emit m_mockTcpServer->clientConnected(m_clientId); if (connectedSpy.count() == 0) connectedSpy.wait(); QCOMPARE(connectedSpy.count(), 1); qCDebug(dcTests()) << "Mock client connected"; spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (spy.count() == 0) { spy.wait(); } QVERIFY(spy.count() == 1); // Now lets authenticate with a wrong user spy.clear(); qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad user)"; 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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad password)"; 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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "Calling Authenticate, expecting success"; 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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "Calling Version, expecting success"; 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(); qCDebug(dcTests()) << "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, &MockTcpServer::outgoingData); QVERIFY(spy.isValid()); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); QVERIFY(disconnectedSpy.isValid()); QSignalSpy connectedSpy(m_mockTcpServer, &MockTcpServer::clientConnected); QVERIFY(connectedSpy.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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "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(); qCDebug(dcTests()) << "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(); disconnectedSpy.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(); qCDebug(dcTests()) << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized")); // And connection should drop if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); // Connect again to not impact subsequent tests... connectedSpy.clear(); emit m_mockTcpServer->clientConnected(m_clientId); if (connectedSpy.count() == 0) connectedSpy.wait(); QCOMPARE(connectedSpy.count(), 1); injectAndWait("JSONRPC.Hello"); } void TestJSONRPC::testBasicCall_data() { QTest::addColumn("call"); QTest::addColumn("idValid"); QTest::addColumn("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\", \"token\": \"" + m_apiToken + "\"}\n") << true << false; QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\", \"token\": \"" + m_apiToken + "\"}\n") << true << false; QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\", \"token\": \"" + m_apiToken + "\"}\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("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", mockDeviceHttpportParamTypeId); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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", mockIntStateTypeId); 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", mockWithoutParamsActionTypeId); actionNoParams.insert("deviceId", m_mockDeviceId); actionNoParams.insert("ruleActionParams", QVariantList()); // EventDescriptor QVariantMap eventDescriptor; eventDescriptor.insert("eventTypeId", mockEvent1EventTypeId); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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", mockIntStateTypeId); 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", mockWithoutParamsActionTypeId); 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); if (clientSpy.count() == 0) 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(mockIntStateTypeId.toString()).arg(20))); QNetworkReply *reply = nam.get(request); connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater())); if (spy.count() == 0) 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(mockIntStateTypeId.toString()).arg(42))); QNetworkReply *reply2 = nam.get(request2); if (spy.count() == 0) spy.wait(); QCOMPARE(spy.count(), 1); connect(reply2, SIGNAL(finished()), reply2, SLOT(deleteLater())); if (clientSpy.count() == 0) 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); if (clientSpy.count() == 0) 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", mockDeviceHttpportParamTypeId); 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()); if (clientSpy.count() == 0) 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", mockDeviceHttpportParamTypeId); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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); if (clientSpy.count() == 0) clientSpy.wait(); 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); if (clientSpy.count() == 0) 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())); if (replySpy.count() == 0) 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", mockDevicePluginConfigParamIntParamTypeId); 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); QSignalSpy responseSpy(m_mockTcpServer, &MockTcpServer::outgoingData); m_mockTcpServer->injectData(malloryId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (responseSpy.count() == 0) responseSpy.wait(); // 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, &MockTcpServer::outgoingData); // Create a new clientId for alice and connect it to the server QUuid aliceId = QUuid::createUuid(); m_mockTcpServer->clientConnected(aliceId); m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (clientSpy.count() == 0) clientSpy.wait(); // 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); clientSpy.clear(); m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (clientSpy.count() == 0) clientSpy.wait(); // 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, &MockTcpServer::outgoingData); QVERIFY(spy.isValid()); PushButtonAgent pushButtonAgent; pushButtonAgent.init(); // Hello call should work in any case, telling us initial setup is required spy.clear(); qCDebug(dcTests()) << "Calling Hello on uninitialized instance"; 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(); qCDebug(dcTests()) << "Result:" << 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); spy.clear(); m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (spy.count() == 0) spy.wait(); 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(); qCDebug(dcTests()) << "Calling Hello on uninitialized instance"; 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(); qCDebug(dcTests()) << "Result:" << 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(); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); qCDebug(dcTests()) << "Calling CreateUser on uninitialized instance"; 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(); qCDebug(dcTests()) << "Result:" << 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); // Connection should drop if (disconnectedSpy.isEmpty()) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); // Reconnect to not impact subsequent tests m_mockTcpServer->clientConnected(m_clientId); spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); if (spy.isEmpty()) spy.wait(); } void TestJSONRPC::testDataFragmentation_data() { QTest::addColumn >("packets"); QList 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, 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 && spy.count() == 0; i ++) { m_mockTcpServer->injectData(m_clientId, data); } QCOMPARE(spy.count(), 1); } #include "testjsonrpc.moc" QTEST_MAIN(TestJSONRPC)