diff --git a/server/jsonrpc/jsonrpcserver.cpp b/server/jsonrpc/jsonrpcserver.cpp index f2ae03a6..dbcfc459 100644 --- a/server/jsonrpc/jsonrpcserver.cpp +++ b/server/jsonrpc/jsonrpcserver.cpp @@ -113,6 +113,19 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject returns.insert("o:token", JsonTypes::basicTypeToString(JsonTypes::String)); setReturns("Authenticate", returns); + params.clear(); returns.clear(); + setDescription("Tokens", "Return a list of if TokenInfo objects all the tokens for the current user."); + setParams("Tokens", params); + returns.insert("tokenInfoList", QVariantList() << JsonTypes::tokenInfoRef()); + setReturns("Tokens", returns); + + params.clear(); returns.clear(); + setDescription("RemoveToken", "Revoke access for a given token."); + params.insert("tokenId", JsonTypes::basicTypeToString(JsonTypes::Uuid)); + setParams("RemoveToken", params); + returns.insert("error", JsonTypes::userErrorRef()); + setReturns("RemoveToken", returns); + QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); } @@ -189,6 +202,35 @@ JsonReply *JsonRPCServer::Authenticate(const QVariantMap ¶ms) return createReply(ret); } +JsonReply *JsonRPCServer::Tokens(const QVariantMap ¶ms) const +{ + Q_UNUSED(params) + QByteArray token = property("token").toByteArray(); + + QString username = GuhCore::instance()->userManager()->userForToken(token); + if (username.isEmpty()) { + // There *really* should be a user for the token in the DB + Q_ASSERT(false); + } + QList tokens = GuhCore::instance()->userManager()->tokens(username); + QVariantList retList; + foreach (const TokenInfo &tokenInfo, tokens) { + retList << JsonTypes::packTokenInfo(tokenInfo); + } + QVariantMap retMap; + retMap.insert("tokenInfoList", retList); + return createReply(retMap); +} + +JsonReply *JsonRPCServer::RemoveToken(const QVariantMap ¶ms) +{ + QUuid tokenId = params.value("tokenId").toUuid(); + UserManager::UserError error = GuhCore::instance()->userManager()->removeToken(tokenId); + QVariantMap ret; + ret.insert("error", JsonTypes::userErrorToString(error)); + return createReply(ret); +} + /*! Returns the list of registred \l{JsonHandler}{JsonHandlers} and their name.*/ QHash JsonRPCServer::handlers() const { @@ -324,6 +366,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) // Hack: attach clientId to handler to be able to handle the JSONRPC methods. Do not use this outside of jsonrpcserver handler->setProperty("clientId", clientId); + handler->setProperty("token", message.value("token").toByteArray()); JsonReply *reply; QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params)); diff --git a/server/jsonrpc/jsonrpcserver.h b/server/jsonrpc/jsonrpcserver.h index f46bcd28..212fbb1f 100644 --- a/server/jsonrpc/jsonrpcserver.h +++ b/server/jsonrpc/jsonrpcserver.h @@ -53,6 +53,8 @@ public: Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); QHash handlers() const; diff --git a/server/jsonrpc/jsontypes.cpp b/server/jsonrpc/jsontypes.cpp index 05ca6ccf..4a385246 100644 --- a/server/jsonrpc/jsontypes.cpp +++ b/server/jsonrpc/jsontypes.cpp @@ -2,6 +2,7 @@ * * * Copyright (C) 2015 Simon Stürz * * Copyright (C) 2014 Michael Zanetti * + * Copyright (C) 2017 Michael Zanetti * * * * This file is part of guh. * * * @@ -117,6 +118,7 @@ QVariantMap JsonTypes::s_repeatingOption; QVariantMap JsonTypes::s_wirelessAccessPoint; QVariantMap JsonTypes::s_wiredNetworkDevice; QVariantMap JsonTypes::s_wirelessNetworkDevice; +QVariantMap JsonTypes::s_tokenInfo; void JsonTypes::init() { @@ -351,6 +353,12 @@ void JsonTypes::init() s_wirelessNetworkDevice.insert("bitRate", basicTypeToString(QVariant::String)); s_wirelessNetworkDevice.insert("o:currentAccessPoint", wirelessAccessPointRef()); + // TokenInfo + s_tokenInfo.insert("id", basicTypeToString(QVariant::Uuid)); + s_tokenInfo.insert("userName", basicTypeToString(QVariant::String)); + s_tokenInfo.insert("deviceName", basicTypeToString(QVariant::String)); + s_tokenInfo.insert("creationTime", basicTypeToString(QVariant::UInt)); + s_initialized = true; } @@ -428,6 +436,7 @@ QVariantMap JsonTypes::allTypes() allTypes.insert("WirelessAccessPoint", wirelessAccessPointDescription()); allTypes.insert("WiredNetworkDevice", wiredNetworkDeviceDescription()); allTypes.insert("WirelessNetworkDevice", wirelessNetworkDeviceDescription()); + allTypes.insert("TokenInfo", tokenInfoDescription()); return allTypes; } @@ -1167,6 +1176,16 @@ QVariantList JsonTypes::packPlugins() return pluginsList; } +QVariantMap JsonTypes::packTokenInfo(const TokenInfo &tokenInfo) +{ + QVariantMap ret; + ret.insert("id", tokenInfo.id().toString()); + ret.insert("userName", tokenInfo.username()); + ret.insert("deviceName", tokenInfo.deviceName()); + ret.insert("creationTime", tokenInfo.creationTime().toTime_t()); + return ret; +} + /*! Returns the type string for the given \a type. */ QString JsonTypes::basicTypeToString(const QVariant::Type &type) { @@ -1794,6 +1813,12 @@ QPair JsonTypes::validateVariant(const QVariant &templateVariant, qCWarning(dcJsonRpc) << "WirelessNetworkDevice not matching"; return result; } + } else if (refName == tokenInfoRef()) { + QPair result = validateMap(tokenInfoDescription(), variant.toMap()); + if (!result.first) { + qCWarning(dcJsonRpc) << "TokenInfo not matching"; + return result; + } } else if (refName == basicTypeRef()) { QPair result = validateBasicType(variant); if (!result.first) { diff --git a/server/jsonrpc/jsontypes.h b/server/jsonrpc/jsontypes.h index beda25f6..c60a2c54 100644 --- a/server/jsonrpc/jsontypes.h +++ b/server/jsonrpc/jsontypes.h @@ -165,6 +165,7 @@ public: DECLARE_OBJECT(wirelessAccessPoint, "WirelessAccessPoint") DECLARE_OBJECT(wiredNetworkDevice, "WiredNetworkDevice") DECLARE_OBJECT(wirelessNetworkDevice, "WirelessNetworkDevice") + DECLARE_OBJECT(tokenInfo, "TokenInfo") // pack types static QVariantMap packEventType(const EventType &eventType); @@ -197,7 +198,6 @@ public: static QVariantMap packWiredNetworkDevice(WiredNetworkDevice *networkDevice); static QVariantMap packWirelessNetworkDevice(WirelessNetworkDevice *networkDevice); - // pack resources static QVariantList packRules(const QList rules); static QVariantList packCreateMethods(DeviceClass::CreateMethods createMethods); static QVariantList packSupportedVendors(); @@ -219,6 +219,8 @@ public: static QVariantList packEventTypes(const DeviceClass &deviceClass); static QVariantList packPlugins(); + static QVariantMap packTokenInfo(const TokenInfo &tokenInfo); + static QString basicTypeToString(const QVariant::Type &type); // unpack Types diff --git a/server/server.pri b/server/server.pri index 856f4049..17c1f5a3 100644 --- a/server/server.pri +++ b/server/server.pri @@ -57,6 +57,7 @@ HEADERS += $$top_srcdir/server/guhcore.h \ $$top_srcdir/server/networkmanager/networkconnection.h \ $$top_srcdir/server/networkmanager/wirednetworkdevice.h \ $$top_srcdir/server/usermanager.h \ + $$PWD/tokeninfo.h SOURCES += $$top_srcdir/server/guhcore.cpp \ @@ -113,4 +114,5 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/networkmanager/networkconnection.cpp \ $$top_srcdir/server/networkmanager/wirednetworkdevice.cpp \ $$top_srcdir/server/usermanager.cpp \ + $$PWD/tokeninfo.cpp diff --git a/server/tokeninfo.cpp b/server/tokeninfo.cpp new file mode 100644 index 00000000..8153374a --- /dev/null +++ b/server/tokeninfo.cpp @@ -0,0 +1,54 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Michael Zanetti * + * * + * This file is part of guh. * + * * + * Guh 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. * + * * + * Guh is distributed in the hope that it will be useful, * + * 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 guh. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tokeninfo.h" + +namespace guhserver { + +TokenInfo::TokenInfo(const QUuid &id, const QString &username, const QDateTime &creationTime, const QString &deviceName): + m_id(id), + m_username(username), + m_creationTime(creationTime), + m_deviceName(deviceName) +{ + +} + +QUuid TokenInfo::id() const +{ + return m_id; +} + +QString TokenInfo::username() const +{ + return m_username; +} + +QDateTime TokenInfo::creationTime() const +{ + return m_creationTime; +} + +QString TokenInfo::deviceName() const +{ + return m_deviceName; +} + +} diff --git a/server/tokeninfo.h b/server/tokeninfo.h new file mode 100644 index 00000000..c207b6b6 --- /dev/null +++ b/server/tokeninfo.h @@ -0,0 +1,48 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Michael Zanetti * + * * + * This file is part of guh. * + * * + * Guh 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. * + * * + * Guh is distributed in the hope that it will be useful, * + * 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 guh. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TOKENINFO_H +#define TOKENINFO_H + +#include +#include + +namespace guhserver { + +class TokenInfo +{ +public: + TokenInfo(const QUuid &id, const QString &username, const QDateTime &creationTime, const QString &deviceName); + + QUuid id() const; + QString username() const; + QDateTime creationTime() const; + QString deviceName() const; + +private: + QUuid m_id; + QString m_username; + QDateTime m_creationTime; + QString m_deviceName; +}; + +} + +#endif // TOKENINFO_H diff --git a/server/usermanager.cpp b/server/usermanager.cpp index 5dfa882e..b7a6b41e 100644 --- a/server/usermanager.cpp +++ b/server/usermanager.cpp @@ -126,7 +126,8 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas } QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); - QString storeTokenQuery = QString("INSERT INTO tokens(username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\");") + QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");") + .arg(QUuid::createUuid().toString()) .arg(username) .arg(QString::fromUtf8(token)) .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) @@ -140,10 +141,70 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas return token; } +QString UserManager::userForToken(const QByteArray &token) const +{ + if (!validateToken(token)) { + qCWarning(dcUserManager) << "Token failed character validation:" << token; + return QString(); + } + QString getUserQuery = QString("SELECT * FROM tokens WHERE token = \"%1\";") + .arg(QString::fromUtf8(token)); + QSqlQuery result = m_db.exec(getUserQuery); + if (m_db.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Error fetching username for token:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery; + return QString(); + } + if (!result.first()) { + qCWarning(dcUserManager) << "No such token in DB:" << token; + return QString(); + } + + return result.value("username").toString(); +} + +QList UserManager::tokens(const QString &username) const +{ + QList ret; + + if (!validateUsername(username)) { + qCWarning(dcUserManager) << "Username did not pass validation:" << username; + return ret; + } + QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE username = \"%1\";") + .arg(username); + QSqlQuery result = m_db.exec(getTokensQuery); + if (m_db.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery; + return ret; + } + + while (result.next()) { + ret << TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString()); + } + return ret; +} + +UserManager::UserError UserManager::removeToken(const QUuid &tokenId) +{ + QString removeTokenQuery = QString("DELETE FROM tokens WHERE id = \"%1\";") + .arg(tokenId.toString()); + QSqlQuery result = m_db.exec(removeTokenQuery); + if (m_db.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Removing token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << removeTokenQuery; + return UserErrorBackendError; + } + if (result.numRowsAffected() != 1) { + qCWarning(dcUserManager) << "Token not found in DB"; + return UserErrorTokenNotFound; + } + + qCDebug(dcUserManager) << "Token" << tokenId << "removed from DB"; + return UserErrorNoError; +} + bool UserManager::verifyToken(const QByteArray &token) { - QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)")); - if (!validator.exactMatch(token)) { + if (!validateToken(token)) { qCWarning(dcUserManager) << "Token failed character validation" << token; return false; } @@ -168,7 +229,7 @@ void UserManager::initDB() m_db.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE, password VARCHAR(100), salt VARCHAR(100));"); } if (!m_db.tables().contains("tokens")) { - m_db.exec("CREATE TABLE tokens (username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));"); + m_db.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));"); } } @@ -178,4 +239,10 @@ bool UserManager::validateUsername(const QString &username) const return validator.exactMatch(username); } +bool UserManager::validateToken(const QByteArray &token) const +{ + QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)")); + return validator.exactMatch(token); +} + } diff --git a/server/usermanager.h b/server/usermanager.h index b49731f0..7f45472b 100644 --- a/server/usermanager.h +++ b/server/usermanager.h @@ -21,6 +21,8 @@ #ifndef USERMANAGER_H #define USERMANAGER_H +#include "tokeninfo.h" + #include #include @@ -35,7 +37,9 @@ public: UserErrorBackendError, UserErrorInvalidUserId, UserErrorDuplicateUserId, - UserErrorBadPassword + UserErrorBadPassword, + UserErrorTokenNotFound, + UserErrorPermissionDenied }; Q_ENUM(UserError) @@ -47,12 +51,16 @@ public: UserError removeUser(const QString &username); QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName); + QString userForToken(const QByteArray &token) const; + QList tokens(const QString &username) const; + guhserver::UserManager::UserError removeToken(const QUuid &tokenId); bool verifyToken(const QByteArray &token); private: void initDB(); bool validateUsername(const QString &username) const; + bool validateToken(const QByteArray &token) const; private: QSqlDatabase m_db; diff --git a/tests/auto/api.json b/tests/auto/api.json index ec70cdb0..6f949828 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -403,6 +403,15 @@ "types": "Object" } }, + "JSONRPC.RemoveToken": { + "description": "Revoke access for a given token.", + "params": { + "tokenId": "Uuid" + }, + "returns": { + "error": "$ref:UserError" + } + }, "JSONRPC.SetNotificationStatus": { "description": "Enable/Disable notifications for this connections.", "params": { @@ -412,6 +421,16 @@ "enabled": "Bool" } }, + "JSONRPC.Tokens": { + "description": "Return a list of if TokenInfo objects all the tokens for the current user.", + "params": { + }, + "returns": { + "tokenInfoList": [ + "$ref:TokenInfo" + ] + } + }, "JSONRPC.Version": { "description": "Version of this Guh/JSONRPC interface.", "params": { @@ -1291,6 +1310,12 @@ "o:repeating": "$ref:RepeatingOption", "o:time": "Time" }, + "TokenInfo": { + "creationTime": "Uint", + "deviceName": "String", + "id": "Uuid", + "userName": "String" + }, "Unit": [ "UnitNone", "UnitSeconds", @@ -1348,7 +1373,9 @@ "UserErrorBackendError", "UserErrorInvalidUserId", "UserErrorDuplicateUserId", - "UserErrorBadPassword" + "UserErrorBadPassword", + "UserErrorTokenNotFound", + "UserErrorPermissionDenied" ], "ValueOperator": [ "ValueOperatorEquals", diff --git a/tests/auto/guhtestbase.cpp b/tests/auto/guhtestbase.cpp index 0c992c06..bac17775 100644 --- a/tests/auto/guhtestbase.cpp +++ b/tests/auto/guhtestbase.cpp @@ -118,6 +118,7 @@ GuhTestBase::GuhTestBase(QObject *parent) : m_mockDevice2Port = 7331 + (qrand() % 1000); QCoreApplication::instance()->setOrganizationName("guh-test"); + GuhCore::instance()->userManager()->removeUser("dummy@guh.io"); GuhCore::instance()->userManager()->createUser("dummy@guh.io", "DummyPW1!"); m_apiToken = GuhCore::instance()->userManager()->authenticate("dummy@guh.io", "DummyPW1!", "testcase"); } diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index 0071caff..c2bd37b5 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -44,6 +44,8 @@ private slots: void testInitialSetup(); + void testRevokeToken(); + void testBasicCall_data(); void testBasicCall(); @@ -255,6 +257,101 @@ void TestJSONRPC::testInitialSetup() } +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\"}"); + 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\"}}"); + 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\"}"); + 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\"}"); + 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() + "\"}}"); + 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\"}"); + 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("call");