feat: add snapshot tests for eventsub messages (#5965)

This commit is contained in:
nerix
2025-02-21 21:33:29 +01:00
committed by GitHub
parent bb4c6d6f6e
commit a86df7087d
17 changed files with 885 additions and 21 deletions
+2
View File
@@ -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!
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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"
}
]
}
+197
View File
@@ -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 <QString>
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<QString, std::string_view, QCompareCaseInsensitive>
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<TwitchChannel> makeMockTwitchChannel(const QString &name)
{
auto chan = std::make_shared<TwitchChannel>(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<qsizetype>(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<QString>
{
public:
void SetUp() override
{
auto param = TestEventSubMessagesP::GetParam();
this->snapshot = testlib::Snapshot::read(CATEGORY, param);
this->mockApplication = std::make_unique<MockApplication>();
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<TwitchChannel> mainChannel;
std::unique_ptr<MockApplication> mockApplication;
std::unique_ptr<testlib::Snapshot> 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<eventsub::lib::Listener> listener =
std::make_unique<eventsub::Connection>();
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
}
+23
View File
@@ -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)
+3
View File
@@ -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