diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba7574c..babb653f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Dev: Updated `googletest` to 1.16.0. (#5942) - Dev: Fixed duplicate CMake configure in clean builds. (#5940) - Dev: BTTV emotes are now loaded as WEBP. (#5957) +- Dev: Added snapshot tests for EventSub. (#5965) ## 2.5.2 diff --git a/lib/twitch-eventsub-ws/src/chrono.cpp b/lib/twitch-eventsub-ws/src/chrono.cpp index bda4477d..cd2cee0e 100644 --- a/lib/twitch-eventsub-ws/src/chrono.cpp +++ b/lib/twitch-eventsub-ws/src/chrono.cpp @@ -20,7 +20,7 @@ boost::json::result_for> date::parse("%FT%TZ", tp); + in >> date::parse("%FT%H:%M:%12SZ", tp); return tp; } diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp index d218192b..a27029e5 100644 --- a/mocks/include/mocks/TwitchIrcServer.hpp +++ b/mocks/include/mocks/TwitchIrcServer.hpp @@ -9,6 +9,7 @@ #include "providers/seventv/SeventvEmotes.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "util/Twitch.hpp" #include @@ -45,8 +46,19 @@ public: ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override { - assert(false && "unimplemented getChannelOrEmpty in mock irc server"); - return {}; + QString query = cleanChannelName(dirtyChannelName); + + auto it = this->mockChannels.find(query); + if (it == this->mockChannels.end()) + { + return Channel::getEmpty(); + } + auto chan = it->second.lock(); + if (!chan) + { + return Channel::getEmpty(); + } + return chan; } void addFakeMessage(const QString &data) override diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index f5057470..dcbc5c86 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -27,6 +27,7 @@ #include class TestIrcMessageHandlerP; +class TestEventSubMessagesP; namespace chatterino { @@ -514,6 +515,7 @@ private: friend class IrcMessageHandler; friend class Commands_E2E_Test; friend class ::TestIrcMessageHandlerP; + friend class ::TestEventSubMessagesP; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index acb2df55..1c949de5 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -27,6 +27,7 @@ #include "singletons/WindowManager.hpp" #include "util/PostToThread.hpp" #include "util/RatelimitBucket.hpp" +#include "util/Twitch.hpp" #include #include @@ -1201,18 +1202,6 @@ std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( return Channel::getEmpty(); } -QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName) -{ - if (dirtyChannelName.startsWith('#')) - { - return dirtyChannelName.mid(1).toLower(); - } - else - { - return dirtyChannelName.toLower(); - } -} - bool TwitchIrcServer::prepareToSend( const std::shared_ptr &channel) { @@ -1544,7 +1533,7 @@ void TwitchIrcServer::sendRawMessage(const QString &rawMessage) ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName) { - auto channelName = this->cleanChannelName(dirtyChannelName); + auto channelName = cleanChannelName(dirtyChannelName); // try get channel ChannelPtr chan = this->getChannelOrEmpty(channelName); @@ -1594,7 +1583,7 @@ ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName) ChannelPtr TwitchIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) { - auto channelName = this->cleanChannelName(dirtyChannelName); + auto channelName = cleanChannelName(dirtyChannelName); std::lock_guard lock(this->channelMutex); diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index fc3b888e..86a2afd6 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -169,8 +169,6 @@ protected: std::shared_ptr getCustomChannel(const QString &channelname); - QString cleanChannelName(const QString &dirtyChannelName); - private: void onMessageSendRequested(const std::shared_ptr &channel, const QString &message, bool &sent); diff --git a/src/providers/twitch/eventsub/Connection.cpp b/src/providers/twitch/eventsub/Connection.cpp index df3be48e..d04136b1 100644 --- a/src/providers/twitch/eventsub/Connection.cpp +++ b/src/providers/twitch/eventsub/Connection.cpp @@ -55,7 +55,10 @@ void Connection::onChannelBan( BanAction action{}; - action.timestamp = std::chrono::steady_clock::now(); + if (!getApp()->isTest()) + { + action.timestamp = std::chrono::steady_clock::now(); + } action.roomID = roomID; action.source = ActionUser{ .id = QString::fromStdString(payload.event.moderatorUserID), @@ -85,6 +88,10 @@ void Connection::onChannelBan( runInGuiThread([action{std::move(action)}, chan{std::move(chan)}] { auto time = QDateTime::currentDateTime(); + if (getApp()->isTest()) + { + time = QDateTime::fromSecsSinceEpoch(0).toUTC(); + } MessageBuilder msg(action, time); msg->flags.set(MessageFlag::PubSub); chan->addOrReplaceTimeout(msg.release(), QDateTime::currentDateTime()); @@ -162,7 +169,11 @@ void Connection::onChannelModerate( return; } - const auto now = QDateTime::currentDateTime(); + auto now = QDateTime::currentDateTime(); + if (getApp()->isTest()) + { + now = QDateTime::fromSecsSinceEpoch(0).toUTC(); + } std::visit( [&](auto &&action) { diff --git a/src/util/Twitch.cpp b/src/util/Twitch.cpp index fa21c858..3582e346 100644 --- a/src/util/Twitch.cpp +++ b/src/util/Twitch.cpp @@ -62,6 +62,16 @@ void stripChannelName(QString &channelName) } } +QString cleanChannelName(const QString &dirtyChannelName) +{ + if (dirtyChannelName.startsWith('#')) + { + return dirtyChannelName.mid(1).toLower(); + } + + return dirtyChannelName.toLower(); +} + std::pair parseUserNameOrID(const QString &input) { if (input.startsWith("id:")) diff --git a/src/util/Twitch.hpp b/src/util/Twitch.hpp index 367b6cc9..10b51ae0 100644 --- a/src/util/Twitch.hpp +++ b/src/util/Twitch.hpp @@ -16,6 +16,9 @@ void stripUserName(QString &userName); // stripChannelName removes any @ prefix or , suffix to make it more suitable for command use void stripChannelName(QString &channelName); +/// Strips a leading `#` and lowercases the name +QString cleanChannelName(const QString &dirtyChannelName); + using ParsedUserName = QString; using ParsedUserID = QString; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 65e619c0..e29ec3c4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -53,6 +53,8 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp ${CMAKE_CURRENT_LIST_DIR}/src/OnceFlag.cpp ${CMAKE_CURRENT_LIST_DIR}/src/IncognitoBrowser.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/EventSubMessages.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp ${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp # Add your new file above this line! diff --git a/tests/snapshots/EventSub/channel-ban/ban.json b/tests/snapshots/EventSub/channel-ban/ban.json new file mode 100644 index 00000000..5c8051cf --- /dev/null +++ b/tests/snapshots/EventSub/channel-ban/ban.json @@ -0,0 +1,131 @@ +{ + "input": { + "banned_at": "2023-05-20T12:30:55.518375571Z", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "ends_at": null, + "is_permanent": true, + "moderator_user_id": "29024944", + "moderator_user_login": "CLIModerator", + "moderator_user_name": "CLIModerator", + "reason": "This is a test event", + "user_id": "40389552", + "user_login": "testFromUser", + "user_name": "testFromUser" + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "CLIModerator" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "CLIModerator" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "banned" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "testFromUser" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testFromUser:" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "\"This", + "is", + "a", + "test", + "event\"." + ] + } + ], + "flags": "System|Timeout|PubSub", + "id": "", + "localizedName": "", + "loginName": "CLIModerator", + "messageText": "CLIModerator banned testFromUser: \"This is a test event\". ", + "searchText": "CLIModerator banned testFromUser: \"This is a test event\". ", + "serverReceivedTime": "1970-01-01T00:00:00Z", + "timeoutUser": "testFromUser", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/EventSub/channel-ban/timeout.json b/tests/snapshots/EventSub/channel-ban/timeout.json new file mode 100644 index 00000000..437d2596 --- /dev/null +++ b/tests/snapshots/EventSub/channel-ban/timeout.json @@ -0,0 +1,134 @@ +{ + "input": { + "banned_at": "2023-05-20T12:30:55.518375571Z", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "ends_at": "2023-05-20T12:40:55.518375571Z", + "is_permanent": false, + "moderator_user_id": "29024944", + "moderator_user_login": "CLIModerator", + "moderator_user_name": "CLIModerator", + "reason": "This is a test event", + "user_id": "40389552", + "user_login": "testFromUser", + "user_name": "testFromUser" + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "CLIModerator" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "CLIModerator" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "timed", + "out" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "testFromUser" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "testFromUser" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "for", + "10m:", + "\"This", + "is", + "a", + "test", + "event\"." + ] + } + ], + "flags": "System|Timeout|PubSub", + "id": "", + "localizedName": "", + "loginName": "CLIModerator", + "messageText": "CLIModerator timed out testFromUser for 10m: \"This is a test event\". ", + "searchText": "CLIModerator timed out testFromUser for 10m: \"This is a test event\". ", + "serverReceivedTime": "1970-01-01T00:00:00Z", + "timeoutUser": "testFromUser", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/EventSub/channel-moderate/warn-shared.json b/tests/snapshots/EventSub/channel-moderate/warn-shared.json new file mode 100644 index 00000000..41095094 --- /dev/null +++ b/tests/snapshots/EventSub/channel-moderate/warn-shared.json @@ -0,0 +1,174 @@ +{ + "input": { + "action": "warn", + "automod_terms": null, + "ban": null, + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "delete": null, + "followers": null, + "moderator_user_id": "424596340", + "moderator_user_login": "quotrok", + "moderator_user_name": "quotrok", + "raid": null, + "shared_chat_ban": null, + "shared_chat_delete": null, + "shared_chat_timeout": null, + "shared_chat_unban": null, + "shared_chat_untimeout": null, + "slow": null, + "source_broadcaster_user_id": "41292030", + "source_broadcaster_user_login": "adflynn404", + "source_broadcaster_user_name": "adflynn404", + "timeout": null, + "unban": null, + "unban_request": null, + "unmod": null, + "unraid": null, + "untimeout": null, + "unvip": null, + "vip": null, + "warn": { + "chat_rules_cited": null, + "reason": "cut it out", + "user_id": "141981764", + "user_login": "twitchdev", + "user_name": "TwitchDev" + } + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "quotrok", + "words": [ + "quotrok" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has", + "warned" + ] + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "twitchdev", + "words": [ + "TwitchDev" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + ":" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cut", + "it", + "out" + ] + } + ], + "flags": "System|Timeout", + "id": "", + "localizedName": "", + "loginName": "quotrok", + "messageText": "quotrok has warned twitchdev: cut it out ", + "searchText": "quotrok has warned twitchdev: cut it out ", + "serverReceivedTime": "1970-01-01T00:00:00Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/EventSub/channel-moderate/warn.json b/tests/snapshots/EventSub/channel-moderate/warn.json new file mode 100644 index 00000000..fc067443 --- /dev/null +++ b/tests/snapshots/EventSub/channel-moderate/warn.json @@ -0,0 +1,174 @@ +{ + "input": { + "action": "warn", + "automod_terms": null, + "ban": null, + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "delete": null, + "followers": null, + "moderator_user_id": "424596340", + "moderator_user_login": "quotrok", + "moderator_user_name": "quotrok", + "raid": null, + "shared_chat_ban": null, + "shared_chat_delete": null, + "shared_chat_timeout": null, + "shared_chat_unban": null, + "shared_chat_untimeout": null, + "slow": null, + "source_broadcaster_user_id": "11148817", + "source_broadcaster_user_login": "pajlada", + "source_broadcaster_user_name": "pajlada", + "timeout": null, + "unban": null, + "unban_request": null, + "unmod": null, + "unraid": null, + "untimeout": null, + "unvip": null, + "vip": null, + "warn": { + "chat_rules_cited": null, + "reason": "cut it out", + "user_id": "141981764", + "user_login": "twitchdev", + "user_name": "TwitchDev" + } + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "0:00" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "00:00:00", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "quotrok", + "words": [ + "quotrok" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has", + "warned" + ] + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": false, + "type": "MentionElement", + "userColor": "System", + "userLoginName": "twitchdev", + "words": [ + "TwitchDev" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + ":" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cut", + "it", + "out" + ] + } + ], + "flags": "System|Timeout", + "id": "", + "localizedName": "", + "loginName": "quotrok", + "messageText": "quotrok has warned twitchdev: cut it out ", + "searchText": "quotrok has warned twitchdev: cut it out ", + "serverReceivedTime": "1970-01-01T00:00:00Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/src/EventSubMessages.cpp b/tests/src/EventSubMessages.cpp new file mode 100644 index 00000000..44d37a78 --- /dev/null +++ b/tests/src/EventSubMessages.cpp @@ -0,0 +1,197 @@ +#include "common/Literals.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "lib/Snapshot.hpp" +#include "messages/Message.hpp" +#include "mocks/BaseApplication.hpp" +#include "mocks/Logging.hpp" +#include "mocks/TwitchIrcServer.hpp" +#include "providers/twitch/eventsub/Connection.hpp" +#include "Test.hpp" +#include "util/QCompareCaseInsensitive.hpp" + +#include + +using namespace chatterino; +using namespace literals; + +namespace { + +/// Controls whether snapshots will be updated (true) or verified (false) +/// +/// In CI, all snapshots must be verified, thus the integrity tests checks for +/// this constant. +/// +/// When adding a test, start with `{ "input": {...} }` and set this to `true` +/// to generate an initial snapshot. Make sure to verify the output! +constexpr bool UPDATE_SNAPSHOTS = false; + +const QString CATEGORY = u"EventSub"_s; + +/// JSON for the `subscription` object in `payload` +const std::map + SUBSCRIPTIONS{ + { + "channel-moderate", + R"({ + "id": "7297f7eb-3bf5-461f-8ae6-7cd7781ebce3", + "status": "enabled", + "type": "channel.moderate", + "version": "2", + "cost": 0, + "condition": { + "broadcaster_user_id": "11148817", + "moderator_user_id": "11148817" + }, + "transport": { + "method": "websocket", + "session_id": "AgoQUlB8aB2SSsavWVfcs5ljnBIGY2VsbC1j" + }, + "created_at": "2024-02-23T21:12:33.771005262Z" + })", + }, + { + "channel-ban", + R"({ + "id": "4aa632e0-fca3-590b-e981-bbd12abdb3fe", + "status": "enabled", + "type": "channel.ban", + "version": "1", + "condition": { "broadcaster_user_id": "11148817" }, + "transport": { "method": "websocket", "session_id": "38de428e_b11f07be" }, + "created_at": "2023-05-20T12:30:55.518375571Z", + "cost": 0 + })", + }, + }; + +class MockApplication : public mock::BaseApplication +{ +public: + MockApplication() = default; + + ILogging *getChatLogger() override + { + return &this->logging; + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + AccountController *getAccounts() override + { + return &this->accounts; + } + + mock::EmptyLogging logging; + mock::MockTwitchIrcServer twitch; + AccountController accounts; +}; + +std::shared_ptr makeMockTwitchChannel(const QString &name) +{ + auto chan = std::make_shared(name); + return chan; +} + +boost::beast::flat_buffer makePayload(std::string_view subJson, + const QJsonObject &event) +{ + auto subscription = + QJsonDocument::fromJson( + QByteArray::fromRawData(subJson.data(), + static_cast(subJson.size()))) + .object(); + QJsonObject metadata{ + {"message_id", "e8edc592-5550-4aa5-bba6-39e31a2be435"}, + {"message_type", "notification"}, + {"message_timestamp", "2024-05-14T12:31:47.995298776Z"}, + {"subscription_type", subscription["type"]}, + {"subscription_version", subscription["version"]}, + }; + QJsonObject payload{ + {"metadata", metadata}, + {"payload", + QJsonObject{ + {"subscription", subscription}, + {"event", event}, + }}, + }; + + auto bytes = QJsonDocument(payload).toJson(); + + boost::beast::flat_buffer buf; + auto inner = buf.prepare(bytes.size()); + std::memcpy(inner.data(), bytes.data(), inner.size()); + buf.commit(inner.size()); + return buf; +} + +} // namespace + +class TestEventSubMessagesP : public ::testing::TestWithParam +{ +public: + void SetUp() override + { + auto param = TestEventSubMessagesP::GetParam(); + this->snapshot = testlib::Snapshot::read(CATEGORY, param); + + this->mockApplication = std::make_unique(); + + this->mainChannel = makeMockTwitchChannel("pajlada"); + this->mainChannel->setRoomId("11148817"); + + this->mockApplication->twitch.mockChannels.emplace("pajlada", + this->mainChannel); + } + + void TearDown() override + { + this->mainChannel.reset(); + this->mockApplication.reset(); + this->snapshot.reset(); + } + + std::shared_ptr mainChannel; + std::unique_ptr mockApplication; + std::unique_ptr snapshot; +}; + +TEST_P(TestEventSubMessagesP, Run) +{ + QStringView subcategory(snapshot->name()); + subcategory = subcategory.sliced(0, subcategory.indexOf('/')); + auto subscription = SUBSCRIPTIONS.find(subcategory); + ASSERT_NE(subscription, SUBSCRIPTIONS.end()) << subcategory; + + auto json = makePayload(subscription->second, snapshot->input().toObject()); + + std::unique_ptr listener = + std::make_unique(); + auto ec = eventsub::lib::handleMessage(listener, json); + ASSERT_FALSE(ec.failed()) + << ec.what() << ec.message() << ec.location().to_string(); + + auto messages = mainChannel->getMessageSnapshot(); + QJsonArray output; + for (const auto &message : messages) + { + output.append(message->toJson()); + } + + ASSERT_TRUE(snapshot->run(output, UPDATE_SNAPSHOTS)) + << "Snapshot " << snapshot->name() << " failed. Expected JSON to be\n" + << QJsonDocument(snapshot->output().toArray()).toJson() << "\nbut got\n" + << QJsonDocument(output).toJson() << "\ninstead."; +} + +INSTANTIATE_TEST_SUITE_P( + EventSubMessages, TestEventSubMessagesP, + testing::ValuesIn(testlib::Snapshot::discoverNested(CATEGORY))); + +TEST(TestEventSubMessagesP, Integrity) +{ + ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested +} diff --git a/tests/src/lib/Snapshot.cpp b/tests/src/lib/Snapshot.cpp index fb474910..f6857cad 100644 --- a/tests/src/lib/Snapshot.cpp +++ b/tests/src/lib/Snapshot.cpp @@ -175,6 +175,29 @@ QStringList Snapshot::discover(const QString &category) return files; } +QStringList Snapshot::discoverNested(const QString &category) +{ + auto directories = + baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Dirs); + QStringList all; + for (const auto &dir : directories) + { + auto d = baseDir(category); + d.cd(dir); + auto files = d.entryList(QDir::NoDotAndDotDot | QDir::Files); + for (const auto &file : files) + { + QStringView view(file); + if (view.endsWith(u".json")) + { + view = view.sliced(0, view.size() - 5); + } + all.append(dir % u'/' % view); + } + } + return all; +} + bool Snapshot::run(const QJsonValue &got, bool updateSnapshots) const { if (updateSnapshots) diff --git a/tests/src/lib/Snapshot.hpp b/tests/src/lib/Snapshot.hpp index 39f663ea..e4870baf 100644 --- a/tests/src/lib/Snapshot.hpp +++ b/tests/src/lib/Snapshot.hpp @@ -70,6 +70,9 @@ public: /// Finds all tests in @a category static QStringList discover(const QString &category); + /// Finds all 1-level nested tests in @a category (category/foo/bar) + static QStringList discoverNested(const QString &category); + /// @brief Runs the snapshot test /// /// If @a updateSnapshots is `false`, this checks that @a got matches the