From a6f4ddf18822c54a94968ab503ab5b99e0b100c5 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 29 Jun 2018 17:37:21 +0200 Subject: [PATCH] Implement a tagging system --- libnymea-core/jsonrpc/jsonhandler.cpp | 8 + libnymea-core/jsonrpc/jsonhandler.h | 1 + libnymea-core/jsonrpc/jsonrpcserver.cpp | 2 + libnymea-core/jsonrpc/jsontypes.cpp | 54 +++++- libnymea-core/jsonrpc/jsontypes.h | 7 + libnymea-core/jsonrpc/tagshandler.cpp | 143 ++++++++++++++ libnymea-core/jsonrpc/tagshandler.h | 54 ++++++ libnymea-core/libnymea-core.pro | 10 +- libnymea-core/nymeacore.cpp | 10 + libnymea-core/nymeacore.h | 3 + libnymea-core/tagging/tag.cpp | 95 +++++++++ libnymea-core/tagging/tag.h | 57 ++++++ libnymea-core/tagging/tagsstorage.cpp | 185 ++++++++++++++++++ libnymea-core/tagging/tagsstorage.h | 78 ++++++++ libnymea/nymeasettings.cpp | 3 + libnymea/nymeasettings.h | 3 +- nymea.pri | 2 +- tests/auto/api.json | 61 +++++- tests/auto/auto.pro | 1 + tests/auto/jsonrpc/testjsonrpc.cpp | 2 +- tests/auto/nymeatestbase.h | 4 + tests/auto/tags/tags.pro | 5 + tests/auto/tags/testtags.cpp | 248 ++++++++++++++++++++++++ tests/scripts/gettags.sh | 7 + tests/scripts/tagdevice.sh | 7 + 25 files changed, 1043 insertions(+), 7 deletions(-) create mode 100644 libnymea-core/jsonrpc/tagshandler.cpp create mode 100644 libnymea-core/jsonrpc/tagshandler.h create mode 100644 libnymea-core/tagging/tag.cpp create mode 100644 libnymea-core/tagging/tag.h create mode 100644 libnymea-core/tagging/tagsstorage.cpp create mode 100644 libnymea-core/tagging/tagsstorage.h create mode 100644 tests/auto/tags/tags.pro create mode 100644 tests/auto/tags/testtags.cpp create mode 100755 tests/scripts/gettags.sh create mode 100755 tests/scripts/tagdevice.sh diff --git a/libnymea-core/jsonrpc/jsonhandler.cpp b/libnymea-core/jsonrpc/jsonhandler.cpp index 8835a98d..1fe2a3a1 100644 --- a/libnymea-core/jsonrpc/jsonhandler.cpp +++ b/libnymea-core/jsonrpc/jsonhandler.cpp @@ -220,6 +220,14 @@ QVariantMap JsonHandler::statusToReply(NetworkManager::NetworkManagerError statu return returns; } +/*! Returns the formated error map for the given \a status. */ +QVariantMap JsonHandler::statusToReply(TagsStorage::TagError status) const +{ + QVariantMap returns; + returns.insert("tagError", JsonTypes::tagErrorToString(status)); + return returns; +} + /*! \class nymeaserver::JsonReply diff --git a/libnymea-core/jsonrpc/jsonhandler.h b/libnymea-core/jsonrpc/jsonhandler.h index 665d7659..fdb6c9bb 100644 --- a/libnymea-core/jsonrpc/jsonhandler.h +++ b/libnymea-core/jsonrpc/jsonhandler.h @@ -113,6 +113,7 @@ protected: QVariantMap statusToReply(Logging::LoggingError status) const; QVariantMap statusToReply(NymeaConfiguration::ConfigurationError status) const; QVariantMap statusToReply(NetworkManager::NetworkManagerError status) const; + QVariantMap statusToReply(TagsStorage::TagError status) const; private: QHash m_descriptions; diff --git a/libnymea-core/jsonrpc/jsonrpcserver.cpp b/libnymea-core/jsonrpc/jsonrpcserver.cpp index 1cbec9f7..879ead5b 100644 --- a/libnymea-core/jsonrpc/jsonrpcserver.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserver.cpp @@ -57,6 +57,7 @@ #include "websocketserver.h" #include "configurationhandler.h" #include "networkmanagerhandler.h" +#include "tagshandler.h" #include #include @@ -453,6 +454,7 @@ void JsonRPCServer::setup() registerHandler(new StateHandler(this)); registerHandler(new ConfigurationHandler(this)); registerHandler(new NetworkManagerHandler(this)); + registerHandler(new TagsHandler(this)); connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServer::pairingFinished); connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectedChanged, this, &JsonRPCServer::onCloudConnectedChanged); diff --git a/libnymea-core/jsonrpc/jsontypes.cpp b/libnymea-core/jsonrpc/jsontypes.cpp index 5c641508..0609c600 100644 --- a/libnymea-core/jsonrpc/jsontypes.cpp +++ b/libnymea-core/jsonrpc/jsontypes.cpp @@ -89,6 +89,7 @@ QVariantList JsonTypes::s_networkManagerError; QVariantList JsonTypes::s_networkManagerState; QVariantList JsonTypes::s_networkDeviceState; QVariantList JsonTypes::s_userError; +QVariantList JsonTypes::s_tagError; QVariantMap JsonTypes::s_paramType; QVariantMap JsonTypes::s_param; @@ -122,6 +123,7 @@ QVariantMap JsonTypes::s_wirelessNetworkDevice; QVariantMap JsonTypes::s_tokenInfo; QVariantMap JsonTypes::s_serverConfiguration; QVariantMap JsonTypes::s_webServerConfiguration; +QVariantMap JsonTypes::s_tag; void JsonTypes::init() { @@ -148,6 +150,7 @@ void JsonTypes::init() s_networkManagerState = enumToStrings(NetworkManager::staticMetaObject, "NetworkManagerState"); s_networkDeviceState = enumToStrings(NetworkDevice::staticMetaObject, "NetworkDeviceState"); s_userError = enumToStrings(UserManager::staticMetaObject, "UserError"); + s_tagError = enumToStrings(TagsStorage::staticMetaObject, "TagError"); // ParamType s_paramType.insert("id", basicTypeToString(Uuid)); @@ -387,6 +390,13 @@ void JsonTypes::init() s_webServerConfiguration = s_serverConfiguration; s_webServerConfiguration.insert("publicFolder", basicTypeToString(QVariant::String)); + // Tag + s_tag.insert("o:deviceId", basicTypeToString(QVariant::Uuid)); + s_tag.insert("o:ruleId", basicTypeToString(QVariant::Uuid)); + s_tag.insert("appId", basicTypeToString(QVariant::String)); + s_tag.insert("tagId", basicTypeToString(QVariant::String)); + s_tag.insert("o:value", basicTypeToString(QVariant::String)); + s_initialized = true; } @@ -435,6 +445,7 @@ QVariantMap JsonTypes::allTypes() allTypes.insert("NetworkManagerState", networkManagerState()); allTypes.insert("NetworkDeviceState", networkDeviceState()); allTypes.insert("UserError", userError()); + allTypes.insert("TagError", tagErrorRef()); allTypes.insert("StateType", stateTypeDescription()); allTypes.insert("StateDescriptor", stateDescriptorDescription()); @@ -467,6 +478,7 @@ QVariantMap JsonTypes::allTypes() allTypes.insert("TokenInfo", tokenInfoDescription()); allTypes.insert("ServerConfiguration", serverConfigurationDescription()); allTypes.insert("WebServerConfiguration", serverConfigurationDescription()); + allTypes.insert("Tag", tagDescription()); return allTypes; } @@ -946,6 +958,21 @@ QVariantMap JsonTypes::packLogEntry(const LogEntry &logEntry) return logEntryMap; } +/*! Returns a variant map of the given \a tag. */ +QVariantMap JsonTypes::packTag(const Tag &tag) +{ + QVariantMap ret; + if (!tag.deviceId().isNull()){ + ret.insert("deviceId", tag.deviceId()); + } else { + ret.insert("ruleId", tag.ruleId()); + } + ret.insert("appId", tag.appId()); + ret.insert("tagId", tag.tagId()); + ret.insert("value", tag.value()); + return ret; +} + /*! Returns a variant list of the given \a createMethods. */ QVariantList JsonTypes::packCreateMethods(DeviceClass::CreateMethods createMethods) { @@ -1609,6 +1636,20 @@ TimeDescriptor JsonTypes::unpackTimeDescriptor(const QVariantMap &timeDescriptor return timeDescriptor; } +/*! Returns a \l{Tag} created from the given \a tagMap. */ +Tag JsonTypes::unpackTag(const QVariantMap &tagMap) +{ + DeviceId deviceId = DeviceId(tagMap.value("deviceId").toString()); + RuleId ruleId = RuleId(tagMap.value("ruleId").toString()); + QString appId = tagMap.value("appId").toString(); + QString tagId = tagMap.value("tagId").toString(); + QString value = tagMap.value("value").toString(); + if (!deviceId.isNull()) { + return Tag(deviceId, appId, tagId, value); + } + return Tag(ruleId, appId, tagId, value); +} + ServerConfiguration JsonTypes::unpackServerConfiguration(const QVariantMap &serverConfigurationMap) { ServerConfiguration serverConfiguration; @@ -1939,6 +1980,12 @@ QPair JsonTypes::validateVariant(const QVariant &templateVariant, qCWarning(dcJsonRpc) << "WebServerConfiguration not matching"; return result; } + } else if (refName == tagRef()) { + QPair result = validateMap(tagDescription(), variant.toMap()); + if (!result.first) { + qCWarning(dcJsonRpc) << "Tag not matching"; + return result; + } } else if (refName == basicTypeRef()) { QPair result = validateBasicType(variant); if (!result.first) { @@ -2071,11 +2118,16 @@ QPair JsonTypes::validateVariant(const QVariant &templateVariant, qCWarning(dcJsonRpc) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(userErrorRef()); return result; } + } else if (refName == tagErrorRef()) { + QPair result = validateEnum(s_tagError, variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(logEntryRef()); + return result; + } } else { Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data()); return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName)); } - } else { QPair result = JsonTypes::validateProperty(templateVariant, variant); if (!result.first) { diff --git a/libnymea-core/jsonrpc/jsontypes.h b/libnymea-core/jsonrpc/jsontypes.h index 59926148..ad9cf1b2 100644 --- a/libnymea-core/jsonrpc/jsontypes.h +++ b/libnymea-core/jsonrpc/jsontypes.h @@ -42,6 +42,9 @@ #include "logging/logentry.h" #include "logging/logfilter.h" +#include "tagging/tagsstorage.h" +#include "tagging/tag.h" + #include "time/calendaritem.h" #include "time/repeatingoption.h" #include "time/timedescriptor.h" @@ -135,6 +138,7 @@ public: DECLARE_TYPE(networkManagerState, "NetworkManagerState", NetworkManager, NetworkManagerState) DECLARE_TYPE(networkDeviceState, "NetworkDeviceState", NetworkDevice, NetworkDeviceState) DECLARE_TYPE(userError, "UserError", UserManager, UserError) + DECLARE_TYPE(tagError, "TagError", TagsStorage, TagError) DECLARE_OBJECT(paramType, "ParamType") DECLARE_OBJECT(param, "Param") @@ -168,6 +172,7 @@ public: DECLARE_OBJECT(tokenInfo, "TokenInfo") DECLARE_OBJECT(serverConfiguration, "ServerConfiguration") DECLARE_OBJECT(webServerConfiguration, "WebServerConfiguration") + DECLARE_OBJECT(tag, "Tag") // pack types static QVariantMap packEventType(const EventType &eventType); @@ -192,6 +197,7 @@ public: static QVariantMap packRule(const Rule &rule); static QVariantMap packRuleDescription(const Rule &rule); static QVariantMap packLogEntry(const LogEntry &logEntry); + static QVariantMap packTag(const Tag &tag); static QVariantMap packRepeatingOption(const RepeatingOption &option); static QVariantMap packCalendarItem(const CalendarItem &calendarItem); static QVariantMap packTimeEventItem(const TimeEventItem &timeEventItem); @@ -241,6 +247,7 @@ public: static CalendarItem unpackCalendarItem(const QVariantMap &calendarItemMap); static TimeEventItem unpackTimeEventItem(const QVariantMap &timeEventItemMap); static TimeDescriptor unpackTimeDescriptor(const QVariantMap &timeDescriptorMap); + static Tag unpackTag(const QVariantMap &tagMap); static ServerConfiguration unpackServerConfiguration(const QVariantMap &serverConfigurationMap); static WebServerConfiguration unpackWebServerConfiguration(const QVariantMap &webServerConfigurationMap); diff --git a/libnymea-core/jsonrpc/tagshandler.cpp b/libnymea-core/jsonrpc/tagshandler.cpp new file mode 100644 index 00000000..45ef3576 --- /dev/null +++ b/libnymea-core/jsonrpc/tagshandler.cpp @@ -0,0 +1,143 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tagshandler.h" + +#include "nymeacore.h" +#include "tagging/tagsstorage.h" + +TagsHandler::TagsHandler(QObject *parent) : JsonHandler(parent) +{ + QVariantMap params; + QVariantMap returns; + + params.clear(); returns.clear(); + setDescription("GetTags", "Get the Tags matching the given filter. Tags can be filtered by a deviceID, a ruleId, an appId, a tagId or a combination of any (however, combining deviceId and ruleId will return an empty result set)."); + params.insert("o:deviceId", JsonTypes::basicTypeToString(JsonTypes::Uuid)); + params.insert("o:ruleId", JsonTypes::basicTypeToString(JsonTypes::Uuid)); + params.insert("o:appId", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("o:tagId", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("GetTags", params); + returns.insert("tagError", JsonTypes::tagErrorRef()); + returns.insert("o:tags", QVariantList() << JsonTypes::tagRef()); + setReturns("GetTags", returns); + + params.clear(); returns.clear(); + setDescription("AddTag", "Add a Tag. A Tag must have a deviceId OR a ruleId (call this method twice if you want to attach the same tag to a device and a rule), an appId (Use the appId of your app), a tagId (e.g. \"favorites\") and a value. Upon success, a TagAdded notification will be emitted. Calling this method twice for the same ids (device/rule, appId and tagId) but with a different value will update the tag's value and the TagValueChanged notification will be emitted."); + params.insert("tag", JsonTypes::tagRef()); + setParams("AddTag", params); + returns.insert("tagError", JsonTypes::tagErrorRef()); + setReturns("AddTag", returns); + + params.clear(); returns.clear(); + setDescription("RemoveTag", "Remove a Tag. Tag value is optional and will be disregarded. If the ids match, the tag will be deleted and a TagRemoved notification will be emitted."); + params.insert("tag", JsonTypes::tagRef()); + setParams("RemoveTag", params); + returns.insert("tagError", JsonTypes::tagErrorRef()); + setReturns("RemoveTag", returns); + + // Notifications + params.clear(); + setDescription("TagAdded", "Emitted whenever a tag is added to the system. "); + params.insert("tag", JsonTypes::tagRef()); + setParams("TagAdded", params); + connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagAdded, this, &TagsHandler::onTagAdded); + + params.clear(); + setDescription("TagRemoved", "Emitted whenever a tag is removed from the system. "); + params.insert("tag", JsonTypes::tagRef()); + setParams("TagRemoved", params); + connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagRemoved, this, &TagsHandler::onTagRemoved); + + params.clear(); + setDescription("TagValueChanged", "Emitted whenever a tag's value is changed in the system. "); + params.insert("tag", JsonTypes::tagRef()); + setParams("TagValueChanged", params); + connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagValueChanged, this, &TagsHandler::onTagValueChanged); +} + +QString TagsHandler::name() const +{ + return "Tags"; +} + +JsonReply *TagsHandler::GetTags(const QVariantMap ¶ms) const +{ + QVariantList ret; + foreach (const Tag &tag, NymeaCore::instance()->tagsStorage()->tags()) { + if (params.contains("deviceId") && params.value("deviceId").toString() != tag.deviceId().toString()) { + continue; + } + if (params.contains("ruleId") && params.value("ruleId").toString() != tag.ruleId().toString()) { + continue; + } + if (params.contains("appId") && params.value("appId").toString() != tag.appId()) { + continue; + } + if (params.contains("tagId") && params.value("tagId").toString() != tag.tagId()) { + continue; + } + ret.append(JsonTypes::packTag(tag)); + } + QVariantMap returns = statusToReply(TagsStorage::TagErrorNoError); + returns.insert("tags", ret); + return createReply(returns); + +} + +JsonReply *TagsHandler::AddTag(const QVariantMap ¶ms) const +{ + Tag tag = JsonTypes::unpackTag(params.value("tag").toMap()); + TagsStorage::TagError error = NymeaCore::instance()->tagsStorage()->addTag(tag); + QVariantMap returns = statusToReply(error); + return createReply(returns); +} + +JsonReply *TagsHandler::RemoveTag(const QVariantMap ¶ms) const +{ + Tag tag = JsonTypes::unpackTag(params.value("tag").toMap()); + TagsStorage::TagError error = NymeaCore::instance()->tagsStorage()->removeTag(tag); + QVariantMap returns = statusToReply(error); + return createReply(returns); +} + +void TagsHandler::onTagAdded(const Tag &tag) +{ + qCDebug(dcJsonRpc) << "Notify \"Tags.TagAdded\""; + QVariantMap params; + params.insert("tag", JsonTypes::packTag(tag)); + emit TagAdded(params); +} + +void TagsHandler::onTagRemoved(const Tag &tag) +{ + qCDebug(dcJsonRpc) << "Notify \"Tags.TagRemoved\""; + QVariantMap params; + params.insert("tag", JsonTypes::packTag(tag)); + emit TagRemoved(params); +} + +void TagsHandler::onTagValueChanged(const Tag &tag) +{ + qCDebug(dcJsonRpc) << "Notify \"Tags.TagValueChanged\""; + QVariantMap params; + params.insert("tag", JsonTypes::packTag(tag)); + emit TagValueChanged(params); +} diff --git a/libnymea-core/jsonrpc/tagshandler.h b/libnymea-core/jsonrpc/tagshandler.h new file mode 100644 index 00000000..6bbc7124 --- /dev/null +++ b/libnymea-core/jsonrpc/tagshandler.h @@ -0,0 +1,54 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TAGSHANDLER_H +#define TAGSHANDLER_H + +#include + +#include "jsonhandler.h" + +namespace nymeaserver { + +class TagsHandler : public JsonHandler +{ + Q_OBJECT +public: + explicit TagsHandler(QObject *parent = nullptr); + QString name() const override; + + Q_INVOKABLE JsonReply *GetTags(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *AddTag(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *RemoveTag(const QVariantMap ¶ms) const; + +signals: + void TagAdded(const QVariantMap ¶ms); + void TagRemoved(const QVariantMap ¶ms); + void TagValueChanged(const QVariantMap ¶ms); + +private slots: + void onTagAdded(const Tag &tag); + void onTagRemoved(const Tag &tag); + void onTagValueChanged(const Tag &tag); +}; + +} + +#endif // TAGSHANDLER_H diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index d12b23d1..392c48cd 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -93,7 +93,10 @@ HEADERS += nymeacore.h \ hardware/network/avahi/qtavahiservice_p.h \ hardware/network/avahi/qtavahiservicebrowserimplementation.h \ hardware/network/avahi/qtavahiservicebrowserimplementation_p.h \ - debugserverhandler.h + debugserverhandler.h \ + tagging/tagsstorage.h \ + tagging/tag.h \ + jsonrpc/tagshandler.h SOURCES += nymeacore.cpp \ tcpserver.cpp \ @@ -171,4 +174,7 @@ SOURCES += nymeacore.cpp \ hardware/network/avahi/qtavahiservice_p.cpp \ hardware/network/avahi/qtavahiservicebrowserimplementation.cpp \ hardware/network/avahi/qtavahiservicebrowserimplementation_p.cpp \ - debugserverhandler.cpp + debugserverhandler.cpp \ + tagging/tagsstorage.cpp \ + tagging/tag.cpp \ + jsonrpc/tagshandler.cpp diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index 38629b75..b8890813 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -101,6 +101,7 @@ #include "ruleengine.h" #include "networkmanager/networkmanager.h" #include "nymeasettings.h" +#include "tagging/tagsstorage.h" #include "devicemanager.h" #include "plugin/device.h" @@ -485,6 +486,12 @@ DebugServerHandler *NymeaCore::debugServerHandler() const return m_debugServerHandler; } +/*! Returns a pointer to the \l{TagsStorage} instance owned by NymeaCore. */ +TagsStorage *NymeaCore::tagsStorage() const +{ + return m_tagsStorage; +} + /*! Constructs NymeaCore with the given \a parent. This is private. Use \l{NymeaCore::instance()} to access the single instance.*/ @@ -516,6 +523,9 @@ void NymeaCore::init() { qCDebug(dcApplication()) << "Creating User Manager"; m_userManager = new UserManager(NymeaSettings::settingsPath() + "/user-db.sqlite", this); + qCDebug(dcApplication()) << "Creating Tags Storage"; + m_tagsStorage = new TagsStorage(m_deviceManager, m_ruleEngine, this); + qCDebug(dcApplication) << "Creating Server Manager"; m_serverManager = new ServerManager(m_configuration, this); diff --git a/libnymea-core/nymeacore.h b/libnymea-core/nymeacore.h index 95a14816..6c4ce0ee 100644 --- a/libnymea-core/nymeacore.h +++ b/libnymea-core/nymeacore.h @@ -49,6 +49,7 @@ class JsonRPCServer; class LogEngine; class NetworkManager; class NymeaConfiguration; +class TagsStorage; class NymeaCore : public QObject { @@ -84,6 +85,7 @@ public: UserManager *userManager() const; CloudManager *cloudManager() const; DebugServerHandler *debugServerHandler() const; + TagsStorage *tagsStorage() const; static QStringList getAvailableLanguages(); @@ -121,6 +123,7 @@ private: CloudManager *m_cloudManager; HardwareManagerImplementation *m_hardwareManager; DebugServerHandler *m_debugServerHandler; + TagsStorage *m_tagsStorage; NetworkManager *m_networkManager; UserManager *m_userManager; diff --git a/libnymea-core/tagging/tag.cpp b/libnymea-core/tagging/tag.cpp new file mode 100644 index 00000000..a900ab92 --- /dev/null +++ b/libnymea-core/tagging/tag.cpp @@ -0,0 +1,95 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tag.h" + +#include + +namespace nymeaserver { + +Tag::Tag(const DeviceId &deviceId, const QString &appId, const QString &tagId, const QString &value): + m_deviceId(deviceId), + m_appId(appId), + m_tagId(tagId), + m_value(value) +{ + +} + +Tag::Tag(const RuleId &ruleId, const QString &appId, const QString &tagId, const QString &value): + m_ruleId(ruleId), + m_appId(appId), + m_tagId(tagId), + m_value(value) + +{ + +} + +DeviceId Tag::deviceId() const +{ + return m_deviceId; +} + +RuleId Tag::ruleId() const +{ + return m_ruleId; +} + +QString Tag::appId() const +{ + return m_appId; +} + +QString Tag::tagId() const +{ + return m_tagId; +} + +QString Tag::value() const +{ + return m_value; +} + +void Tag::setValue(const QString &value) +{ + m_value = value; +} + +bool Tag::operator==(const Tag &other) const +{ + return m_deviceId == other.deviceId() && + m_ruleId == other.ruleId() && + m_appId == other.appId() && + m_tagId == other.tagId(); +} + +QDebug operator<<(QDebug dbg, const Tag &tag) +{ + if (!tag.deviceId().isNull()) { + dbg.nospace() << "Tag (DeviceId:" << tag.deviceId(); + } else { + dbg.nospace() << "Tag (RuleId:" << tag.ruleId(); + } + dbg.nospace() << ", AppId:" << tag.appId() << ", TagId:" << tag.tagId() << ", Value:" << tag.value() << ")" << endl; + return dbg; +} + +} diff --git a/libnymea-core/tagging/tag.h b/libnymea-core/tagging/tag.h new file mode 100644 index 00000000..78e95b51 --- /dev/null +++ b/libnymea-core/tagging/tag.h @@ -0,0 +1,57 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TAG_H +#define TAG_H + +#include "typeutils.h" + +#include + +namespace nymeaserver { + +class Tag +{ +public: + Tag(const DeviceId &deviceId, const QString &appId, const QString &tagId, const QString &value); + Tag(const RuleId &ruleId, const QString &appId, const QString &tagId, const QString &value); + + DeviceId deviceId() const; + RuleId ruleId() const; + QString appId() const; + QString tagId() const; + + QString value() const; + void setValue(const QString &value); + + bool operator==(const Tag &other) const; + +private: + DeviceId m_deviceId; + RuleId m_ruleId; + QString m_appId; + QString m_tagId; + QString m_value; +}; + +QDebug operator<<(QDebug dbg, const Tag &tag); +} + +#endif // TAG_H diff --git a/libnymea-core/tagging/tagsstorage.cpp b/libnymea-core/tagging/tagsstorage.cpp new file mode 100644 index 00000000..72ecdd4a --- /dev/null +++ b/libnymea-core/tagging/tagsstorage.cpp @@ -0,0 +1,185 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tagsstorage.h" +#include "devicemanager.h" +#include "ruleengine.h" +#include "nymeasettings.h" + +namespace nymeaserver { + +TagsStorage::TagsStorage(DeviceManager *deviceManager, RuleEngine *ruleEngine, QObject *parent): + QObject(parent), + m_deviceManager(deviceManager), + m_ruleEngine(ruleEngine) +{ + connect(deviceManager, &DeviceManager::deviceRemoved, this, &TagsStorage::deviceRemoved); + + NymeaSettings settings(NymeaSettings::SettingsRoleTags); + + settings.beginGroup("Devices"); + foreach (const QString &deviceId, settings.childGroups()) { + settings.beginGroup(deviceId); + foreach (const QString &appId, settings.childGroups()) { + settings.beginGroup(appId); + foreach (const QString &tagId, settings.childKeys()) { + Tag tag(DeviceId(deviceId), appId, tagId, settings.value(tagId).toString()); + m_tags.append(tag); + } + settings.endGroup(); + } + settings.endGroup(); + } + settings.endGroup(); + settings.beginGroup("Rules"); + foreach (const QString &ruleId, settings.childGroups()) { + settings.beginGroup(ruleId); + foreach (const QString &appId, settings.childGroups()) { + settings.beginGroup(appId); + foreach (const QString &tagId, settings.childKeys()) { + Tag tag(RuleId(ruleId), appId, tagId, settings.value(tagId).toString()); + m_tags.append(tag); + } + settings.endGroup(); + } + settings.endGroup(); + } + settings.endGroup(); +} + +QList TagsStorage::tags() const +{ + return m_tags; +} + +QList TagsStorage::tags(const DeviceId &deviceId) const +{ + QList ret; + foreach (const Tag &tag, m_tags) { + if (tag.deviceId() == deviceId) { + ret.append(tag); + } + } + return ret; +} + +QList TagsStorage::tags(const RuleId &ruleId) const +{ + QList ret; + foreach (const Tag &tag, m_tags) { + if (tag.ruleId() == ruleId) { + ret.append(tag); + } + } + return ret; +} + +TagsStorage::TagError TagsStorage::addTag(const Tag &tag) +{ + if (!tag.deviceId().isNull()) { + if (!m_deviceManager->findConfiguredDevice(tag.deviceId())) { + return TagsStorage::TagErrorDeviceNotFound; + } + } else if (!tag.ruleId().isNull()) { + if (!m_ruleEngine->findRule(tag.ruleId()).isValid()) { + return TagsStorage::TagErrorRuleNotFound; + } + } + + int index = m_tags.indexOf(tag); + if (index >= 0) { + m_tags.replace(index, tag); + emit tagValueChanged(tag); + } else { + m_tags.append(tag); + emit tagAdded(tag); + } + saveTag(tag); + return TagsStorage::TagErrorNoError; +} + +TagsStorage::TagError TagsStorage::removeTag(const Tag &tag) +{ + if (!m_tags.contains(tag)) { + return TagErrorTagNotFound; + } + m_tags.removeAll(tag); + unsaveTag(tag); + emit tagRemoved(tag); + return TagErrorNoError; +} + +void TagsStorage::deviceRemoved(const DeviceId &deviceId) +{ + QList tagsToRemove; + foreach (const Tag &tag, m_tags) { + if (tag.deviceId() == deviceId) { + tagsToRemove.append(tag); + } + } + while (!tagsToRemove.isEmpty()) { + removeTag(tagsToRemove.takeFirst()); + } +} + +void TagsStorage::ruleRemoved(const RuleId &ruleId) +{ + QList tagsToRemove; + foreach (const Tag &tag, m_tags) { + if (tag.ruleId() == ruleId) { + tagsToRemove.append(tag); + } + } + while (!tagsToRemove.isEmpty()) { + removeTag(tagsToRemove.takeFirst()); + } +} + +void TagsStorage::saveTag(const Tag &tag) +{ + NymeaSettings settings(NymeaSettings::SettingsRoleTags); + + if (!tag.deviceId().isNull()) { + settings.beginGroup("Devices"); + settings.beginGroup(tag.deviceId().toString()); + } else { + settings.beginGroup("Rules"); + settings.beginGroup(tag.ruleId().toString()); + } + settings.beginGroup(tag.appId()); + settings.setValue(tag.tagId(), tag.value()); +} + +void TagsStorage::unsaveTag(const Tag &tag) +{ + NymeaSettings settings(NymeaSettings::SettingsRoleTags); + + if (!tag.deviceId().isNull()) { + settings.beginGroup("Devices"); + settings.beginGroup(tag.deviceId().toString()); + } else { + settings.beginGroup("Rules"); + settings.beginGroup(tag.deviceId().toString()); + } + settings.beginGroup(tag.appId()); + settings.remove(tag.tagId()); +} + +} diff --git a/libnymea-core/tagging/tagsstorage.h b/libnymea-core/tagging/tagsstorage.h new file mode 100644 index 00000000..bae5c4b1 --- /dev/null +++ b/libnymea-core/tagging/tagsstorage.h @@ -0,0 +1,78 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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. * + * * + * nymea 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 nymea. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TAGSSTORAGE_H +#define TAGSSTORAGE_H + +#include "tag.h" +#include "typeutils.h" + +#include +#include + +class DeviceManager; + +namespace nymeaserver { + +class RuleEngine; + +class TagsStorage : public QObject +{ + Q_OBJECT +public: + enum TagError { + TagErrorNoError, + TagErrorDeviceNotFound, + TagErrorRuleNotFound, + TagErrorTagNotFound + }; + Q_ENUM(TagError) + + explicit TagsStorage(DeviceManager* deviceManager, RuleEngine* ruleEngine, QObject *parent = nullptr); + + TagError addTag(const Tag &tag); + TagError removeTag(const Tag &tag); + + QList tags() const; + QList tags(const DeviceId &deviceId) const; + QList tags(const RuleId &ruleId) const; + +signals: + void tagAdded(const Tag &tag); + void tagRemoved(const Tag &tag); + void tagValueChanged(const Tag &tag); + +private slots: + void deviceRemoved(const DeviceId &deviceId); + void ruleRemoved(const RuleId &ruleId); + +private: + void saveTag(const Tag &tag); + void unsaveTag(const Tag &tag); + +private: + DeviceManager *m_deviceManager; + RuleEngine *m_ruleEngine; + QList m_tags; +}; + +} + +#endif // TAGSSTORAGE_H diff --git a/libnymea/nymeasettings.cpp b/libnymea/nymeasettings.cpp index d15914d6..c00bfe5e 100644 --- a/libnymea/nymeasettings.cpp +++ b/libnymea/nymeasettings.cpp @@ -99,6 +99,9 @@ NymeaSettings::NymeaSettings(const SettingsRole &role, QObject *parent): case SettingsRoleDeviceStates: fileName = "devicestates.conf"; break; + case SettingsRoleTags: + fileName = "tags.conf"; + break; } m_settings = new QSettings(basePath + settingsPrefix + fileName, QSettings::IniFormat, this); } diff --git a/libnymea/nymeasettings.h b/libnymea/nymeasettings.h index 1f948f1a..9faaf83e 100644 --- a/libnymea/nymeasettings.h +++ b/libnymea/nymeasettings.h @@ -40,7 +40,8 @@ public: SettingsRoleRules, SettingsRolePlugins, SettingsRoleGlobal, - SettingsRoleDeviceStates + SettingsRoleDeviceStates, + SettingsRoleTags }; explicit NymeaSettings(const SettingsRole &role = SettingsRoleNone, QObject *parent = nullptr); diff --git a/nymea.pri b/nymea.pri index 502ca16c..78996a06 100644 --- a/nymea.pri +++ b/nymea.pri @@ -6,7 +6,7 @@ NYMEA_PLUGINS_PATH=/usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH')/ # define protocol versions JSON_PROTOCOL_VERSION_MAJOR=1 -JSON_PROTOCOL_VERSION_MINOR=5 +JSON_PROTOCOL_VERSION_MINOR=6 REST_API_VERSION=1 DEFINES += NYMEA_VERSION_STRING=\\\"$${NYMEA_VERSION_STRING}\\\" \ diff --git a/tests/auto/api.json b/tests/auto/api.json index 04d97b26..128d3fd4 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -1,4 +1,4 @@ -1.5 +1.6 { "methods": { "Actions.ExecuteAction": { @@ -789,6 +789,39 @@ "deviceError": "$ref:DeviceError", "o:stateType": "$ref:StateType" } + }, + "Tags.AddTag": { + "description": "Add a Tag. A Tag must have a deviceId OR a ruleId (call this method twice if you want to attach the same tag to a device and a rule), an appId (Use the appId of your app), a tagId (e.g. \"favorites\") and a value. Upon success, a TagAdded notification will be emitted. Calling this method twice for the same ids (device/rule, appId and tagId) but with a different value will update the tag's value and the TagValueChanged notification will be emitted.", + "params": { + "tag": "$ref:Tag" + }, + "returns": { + "tagError": "$ref:TagError" + } + }, + "Tags.GetTags": { + "description": "Get the Tags matching the given filter. Tags can be filtered by a deviceID, a ruleId, an appId, a tagId or a combination of any (however, combining deviceId and ruleId will return an empty result set).", + "params": { + "o:appId": "String", + "o:deviceId": "Uuid", + "o:ruleId": "Uuid", + "o:tagId": "String" + }, + "returns": { + "o:tags": [ + "$ref:Tag" + ], + "tagError": "$ref:TagError" + } + }, + "Tags.RemoveTag": { + "description": "Remove a Tag. Tag value is optional and will be disregarded. If the ids match, the tag will be deleted and a TagRemoved notification will be emitted.", + "params": { + "tag": "$ref:Tag" + }, + "returns": { + "tagError": "$ref:TagError" + } } }, "notifications": { @@ -962,6 +995,24 @@ "params": { "ruleId": "Uuid" } + }, + "Tags.TagAdded": { + "description": "Emitted whenever a tag is added to the system. ", + "params": { + "tag": "$ref:Tag" + } + }, + "Tags.TagRemoved": { + "description": "Emitted whenever a tag is removed from the system. ", + "params": { + "tag": "$ref:Tag" + } + }, + "Tags.TagValueChanged": { + "description": "Emitted whenever a tag's value is changed in the system. ", + "params": { + "tag": "$ref:Tag" + } } }, "types": { @@ -1437,6 +1488,14 @@ "o:unit": "$ref:Unit", "type": "$ref:BasicType" }, + "Tag": { + "appId": "String", + "o:deviceId": "Uuid", + "o:ruleId": "Uuid", + "o:value": "String", + "tagId": "String" + }, + "TagError": "$ref:TagError", "TimeDescriptor": { "o:calendarItems": [ "$ref:CalendarItem" diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index 5a06bc65..9773ae78 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -22,3 +22,4 @@ SUBDIRS = versioning \ configurations \ timemanager \ userloading \ + tags \ diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index fd4dcb52..867243a3 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -503,7 +503,7 @@ void TestJSONRPC::introspect() foreach (const QString &ref, extractRefs(item)) { QString typeId = ref; typeId.remove("$ref:"); - QVERIFY2(types.contains(typeId), QString("Undefined ref: %1").arg(ref).toLatin1().data()); + QVERIFY2(types.contains(typeId), QString("Undefined ref: %1. Did you forget to add it to JsonTypes::allTypes()?").arg(ref).toLatin1().data()); } } } diff --git a/tests/auto/nymeatestbase.h b/tests/auto/nymeatestbase.h index 10ba7ec8..4f9336b0 100644 --- a/tests/auto/nymeatestbase.h +++ b/tests/auto/nymeatestbase.h @@ -154,6 +154,10 @@ protected: verifyError(response, "configurationError", JsonTypes::configurationErrorToString(error)); } + inline void verifyTagError(const QVariant &response, TagsStorage::TagError error = TagsStorage::TagErrorNoError) { + verifyError(response, "tagError", JsonTypes::tagErrorToString(error)); + } + inline void verifyParams(const QVariantList &requestList, const QVariantList &responseList, bool allRequired = true) { if (allRequired) diff --git a/tests/auto/tags/tags.pro b/tests/auto/tags/tags.pro new file mode 100644 index 00000000..9e68fe61 --- /dev/null +++ b/tests/auto/tags/tags.pro @@ -0,0 +1,5 @@ +include(../../../nymea.pri) +include(../autotests.pri) + +TARGET = testtags +SOURCES += testtags.cpp diff --git a/tests/auto/tags/testtags.cpp b/tests/auto/tags/testtags.cpp new file mode 100644 index 00000000..f534608e --- /dev/null +++ b/tests/auto/tags/testtags.cpp @@ -0,0 +1,248 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 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 "nymeacore.h" +#include "devicemanager.h" +#include "mocktcpserver.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace nymeaserver; + +class TestTags: public NymeaTestBase +{ + Q_OBJECT + +private slots: + void addTag_data(); + void addTag(); + + void updateTagValue(); + + void removeTag(); + +private: + QVariantMap createDeviceTag(const QString &deviceId, const QString &appId, const QString &tagId, const QString &value); + bool compareDeviceTag(const QVariantMap &tag, const QString &deviceId, const QString &appId, const QString &tagId, const QString &value); + QVariantMap createRuleTag(const QString &ruleId, const QString &appId, const QString &tagId, const QString &value); + bool comapreRuleTag(const QVariantMap &tag, const QString &ruleId, const QString &appId, const QString &tagId, const QString &value); +}; + +QVariantMap TestTags::createDeviceTag(const QString &deviceId, const QString &appId, const QString &tagId, const QString &value) +{ + QVariantMap tag; + tag.insert("deviceId", deviceId); + tag.insert("appId", appId); + tag.insert("tagId", tagId); + tag.insert("value", value); + return tag; +} + +QVariantMap TestTags::createRuleTag(const QString &ruleId, const QString &appId, const QString &tagId, const QString &value) +{ + QVariantMap tag; + tag.insert("ruleId", ruleId); + tag.insert("appId", appId); + tag.insert("tagId", tagId); + tag.insert("value", value); + return tag; +} + +bool TestTags::compareDeviceTag(const QVariantMap &tag, const QString &deviceId, const QString &appId, const QString &tagId, const QString &value) +{ + return tag.value("deviceId").toString() == deviceId && + tag.value("appId").toString() == appId && + tag.value("tagId").toString() == tagId && + tag.value("value").toString() == value; +} +void TestTags::addTag_data() +{ + QTest::addColumn("deviceId"); + QTest::addColumn("appId"); + QTest::addColumn("tagId"); + QTest::addColumn("value"); + QTest::addColumn("expectedError"); + + QTest::newRow("tagDevice") << m_mockDeviceId << "testtags" << "favorites" << "1" << TagsStorage::TagErrorNoError; + QTest::newRow("invalidDevice") << DeviceId::createDeviceId() << "testtags" << "favorites" << "1" << TagsStorage::TagErrorDeviceNotFound; +} + +void TestTags::addTag() +{ + QFETCH(DeviceId, deviceId); + QFETCH(QString, appId); + QFETCH(QString, tagId); + QFETCH(QString, value); + QFETCH(TagsStorage::TagError, expectedError); + + // enable notificartions + QCOMPARE(enableNotifications(), true); + + // Setup connection to mock client + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + // Create a tag; + QVariantMap params; + params.insert("tag", createDeviceTag(deviceId.toString(), appId, tagId, value)); + QVariant response = injectAndWait("Tags.AddTag", params); + verifyTagError(response, expectedError); + + if (expectedError != TagsStorage::TagErrorNoError) { + // If we expected an error, we can drop out here + return; + } + + // Make sure the TagAdded notification is emitted. + QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap(); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap); + QVERIFY2(compareDeviceTag(notificationTagMap, deviceId.toString(), appId, tagId, value), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1()); + + // Try getting the tag via GetTag + params.clear(); + params.insert("deviceId", deviceId.toString()); + params.insert("appId", appId); + params.insert("tagId", tagId); + response = injectAndWait("Tags.GetTags", params); + QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList(); + QCOMPARE(tagsList.count(), 1); + QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId.toString(), appId, tagId, value), "Fetched tag isn't matching the one we added"); +} + +void TestTags::updateTagValue() +{ + // enable notificartions + QCOMPARE(enableNotifications(), true); + + // Setup connection to mock client + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + QString deviceId = m_mockDeviceId.toString(); + QString appId = "testtags"; + QString tagId = "changedNotificationTag"; + + // Create a Tag + QVariantMap params; + params.insert("tag", createDeviceTag(deviceId, appId, tagId, "1")); + QVariant response = injectAndWait("Tags.AddTag", params); + verifyTagError(response, TagsStorage::TagErrorNoError); + + // Check for TagAdded notification + QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap(); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap); + QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, "1"), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1()); + clientSpy.clear(); + + // Try getting the changed tag via GetTag + params.clear(); + params.insert("deviceId", deviceId); + params.insert("appId", appId); + params.insert("tagId", tagId); + response = injectAndWait("Tags.GetTags", params); + QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList(); + QCOMPARE(tagsList.count(), 1); + QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, "1"), "Fetched tag isn't matching the one we added"); + + // Now update the tag + params.clear(); + params.insert("tag", createDeviceTag(deviceId, appId, tagId, "2")); + response = injectAndWait("Tags.AddTag", params); + verifyTagError(response, TagsStorage::TagErrorNoError); + + // Check for TagAdded notification + notificationTagMap = checkNotification(clientSpy, "Tags.TagValueChanged").toMap().value("params").toMap().value("tag").toMap(); + jsonDoc = QJsonDocument::fromVariant(notificationTagMap); + QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, "2"), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1()); + + // Try getting the changed tag via GetTag + params.clear(); + params.insert("deviceId", deviceId); + params.insert("appId", appId); + params.insert("tagId", tagId); + response = injectAndWait("Tags.GetTags", params); + tagsList = response.toMap().value("params").toMap().value("tags").toList(); + QCOMPARE(tagsList.count(), 1); + QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, "2"), "Fetched tag isn't matching the one we added"); +} + +void TestTags::removeTag() +{ + // enable notificartions + QCOMPARE(enableNotifications(), true); + + // Setup connection to mock client + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + QString deviceId = m_mockDeviceId.toString(); + QString appId = "testtags"; + QString tagId = "removeTagTest"; + QString value = "1"; + + // Create a Tag + QVariantMap params; + params.insert("tag", createDeviceTag(deviceId, appId, tagId, value)); + QVariant response = injectAndWait("Tags.AddTag", params); + verifyTagError(response, TagsStorage::TagErrorNoError); + + // Check for TagAdded notification + QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap(); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap); + QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, value), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1()); + clientSpy.clear(); + + // Try getting the tag via GetTag + params.clear(); + params.insert("deviceId", deviceId); + params.insert("appId", appId); + params.insert("tagId", tagId); + response = injectAndWait("Tags.GetTags", params); + QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList(); + QCOMPARE(tagsList.count(), 1); + QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, value), "Fetched tag isn't matching the one we added"); + + // Now remove the tag + params.clear(); + params.insert("tag", createDeviceTag(deviceId, appId, tagId, QString())); + response = injectAndWait("Tags.RemoveTag", params); + verifyTagError(response, TagsStorage::TagErrorNoError); + + // Check for TagRemoved notification + notificationTagMap = checkNotification(clientSpy, "Tags.TagRemoved").toMap().value("params").toMap().value("tag").toMap(); + jsonDoc = QJsonDocument::fromVariant(notificationTagMap); + QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, QString()), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1()); + + // Try getting the tag via GetTag + params.clear(); + params.insert("deviceId", deviceId); + params.insert("appId", appId); + params.insert("tagId", tagId); + response = injectAndWait("Tags.GetTags", params); + tagsList = response.toMap().value("params").toMap().value("tags").toList(); + QCOMPARE(tagsList.count(), 0); +} + +#include "testtags.moc" +QTEST_MAIN(TestTags) diff --git a/tests/scripts/gettags.sh b/tests/scripts/gettags.sh new file mode 100755 index 00000000..a06c5f52 --- /dev/null +++ b/tests/scripts/gettags.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $1 ]; then + echo "usage: $0 host" +else + (echo '{"id":1, "method":"Tags.GetTags"}'; sleep 1) | nc $1 2222 +fi diff --git a/tests/scripts/tagdevice.sh b/tests/scripts/tagdevice.sh new file mode 100755 index 00000000..a4b58a17 --- /dev/null +++ b/tests/scripts/tagdevice.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z $5 ]; then + echo "usage: $0 host deviceId appId tagId value" +else + (echo '{"id":1, "method":"Tags.AddTag", "params": { "tag": {"deviceId": "'$2'", "appId": "'$3'", "tagId": "'$4'", "value":"'$5'"}}}'; sleep 1) | nc $1 2222 +fi