#include "controllers/highlights/HighlightController.hpp" #include "Application.hpp" #include "BaseSettings.hpp" #include "messages/MessageBuilder.hpp" // for MessageParseArgs #include "providers/twitch/TwitchBadge.hpp" // for Badge #include "providers/twitch/api/Helix.hpp" #include #include #include #include #include #include #include using namespace chatterino; using ::testing::Exactly; class MockApplication : IApplication { public: Theme *getThemes() override { return nullptr; } Fonts *getFonts() override { return nullptr; } Emotes *getEmotes() override { return nullptr; } AccountController *getAccounts() override { return &this->accounts; } HotkeyController *getHotkeys() override { return nullptr; } WindowManager *getWindows() override { return nullptr; } Toasts *getToasts() override { return nullptr; } CommandController *getCommands() override { return nullptr; } NotificationController *getNotifications() override { return nullptr; } HighlightController *getHighlights() override { return &this->highlights; } TwitchIrcServer *getTwitch() override { return nullptr; } ChatterinoBadges *getChatterinoBadges() override { return nullptr; } FfzBadges *getFfzBadges() override { return nullptr; } AccountController accounts; HighlightController highlights; // TODO: Figure this out }; class MockHelix : public IHelix { public: MOCK_METHOD(void, fetchUsers, (QStringList userIds, QStringList userLogins, ResultCallback> successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getUserByName, (QString userName, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getUserById, (QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, fetchUsersFollows, (QString fromId, QString toId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getUserFollowers, (QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, fetchStreams, (QStringList userIds, QStringList userLogins, ResultCallback> successCallback, HelixFailureCallback failureCallback, std::function finallyCallback), (override)); MOCK_METHOD(void, getStreamById, (QString userId, (ResultCallback successCallback), HelixFailureCallback failureCallback, std::function finallyCallback), (override)); MOCK_METHOD(void, getStreamByName, (QString userName, (ResultCallback successCallback), HelixFailureCallback failureCallback, std::function finallyCallback), (override)); MOCK_METHOD(void, fetchGames, (QStringList gameIds, QStringList gameNames, (ResultCallback> successCallback), HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, searchGames, (QString gameName, ResultCallback> successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getGameById, (QString gameId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, createClip, (QString channelId, ResultCallback successCallback, std::function failureCallback, std::function finallyCallback), (override)); MOCK_METHOD(void, getChannel, (QString broadcasterId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, createStreamMarker, (QString broadcasterId, QString description, ResultCallback successCallback, std::function failureCallback), (override)); MOCK_METHOD(void, loadBlocks, (QString userId, ResultCallback> successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, blockUser, (QString targetUserId, std::function successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, unblockUser, (QString targetUserId, std::function successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, updateChannel, (QString broadcasterId, QString gameId, QString language, QString title, std::function successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, manageAutoModMessages, (QString userID, QString msgID, QString action, std::function successCallback, std::function failureCallback), (override)); MOCK_METHOD(void, getCheermotes, (QString broadcasterId, ResultCallback> successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getEmoteSetData, (QString emoteSetId, ResultCallback successCallback, HelixFailureCallback failureCallback), (override)); MOCK_METHOD(void, getChannelEmotes, (QString broadcasterId, ResultCallback> successCallback, HelixFailureCallback failureCallback), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateUserChatColor, (QString userID, QString color, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, deleteChatMessages, (QString broadcasterID, QString moderatorID, QString messageID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, addChannelModerator, (QString broadcasterID, QString userID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, removeChannelModerator, (QString broadcasterID, QString userID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, sendChatAnnouncement, (QString broadcasterID, QString moderatorID, QString message, HelixAnnouncementColor color, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD( void, addChannelVIP, (QString broadcasterID, QString userID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, removeChannelVIP, (QString broadcasterID, QString userID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD( void, unbanUser, (QString broadcasterID, QString moderatorID, QString userID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD( // /raid void, startRaid, (QString fromBroadcasterID, QString toBroadcasterId, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // /raid // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD( // /unraid void, cancelRaid, (QString broadcasterID, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // /unraid // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateEmoteMode, (QString broadcasterID, QString moderatorID, bool emoteMode, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateFollowerMode, (QString broadcasterID, QString moderatorID, boost::optional followerModeDuration, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateNonModeratorChatDelay, (QString broadcasterID, QString moderatorID, boost::optional nonModeratorChatDelayDuration, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateSlowMode, (QString broadcasterID, QString moderatorID, boost::optional slowModeWaitTime, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateSubscriberMode, (QString broadcasterID, QString moderatorID, bool subscriberMode, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateUniqueChatMode, (QString broadcasterID, QString moderatorID, bool uniqueChatMode, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); // update chat settings // /timeout, /ban // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, banUser, (QString broadcasterID, QString moderatorID, QString userID, boost::optional duration, QString reason, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // /timeout, /ban // /w // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, sendWhisper, (QString fromUserID, QString toUserID, QString message, ResultCallback<> successCallback, (FailureCallback failureCallback)), (override)); // /w // /vips // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD( void, getChannelVIPs, (QString broadcasterID, ResultCallback> successCallback, (FailureCallback failureCallback)), (override)); // /vips MOCK_METHOD(void, update, (QString clientId, QString oauthToken), (override)); protected: // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateChatSettings, (QString broadcasterID, QString moderatorID, QJsonObject json, ResultCallback successCallback, (FailureCallback failureCallback)), (override)); }; static QString DEFAULT_SETTINGS = R"!( { "accounts": { "uid117166826": { "username": "testaccount_420", "userID": "117166826", "clientID": "abc", "oauthToken": "def" }, "current": "testaccount_420" }, "highlighting": { "selfHighlight": { "enableSound": true }, "blacklist": [ { "pattern": "zenix", "regex": false } ], "users": [ { "pattern": "pajlada", "showInMentions": false, "alert": false, "sound": false, "regex": false, "case": false, "soundUrl": "", "color": "#7fffffff" }, { "pattern": "testaccount_420", "showInMentions": false, "alert": false, "sound": false, "regex": false, "case": false, "soundUrl": "", "color": "#6fffffff" }, { "pattern": "gempir", "showInMentions": true, "alert": true, "sound": false, "regex": false, "case": false, "soundUrl": "", "color": "#7ff19900" } ], "alwaysPlaySound": true, "highlights": [ { "pattern": "!testmanxd", "showInMentions": true, "alert": true, "sound": true, "regex": false, "case": false, "soundUrl": "", "color": "#7f7f3f49" } ], "badges": [ { "name": "broadcaster", "displayName": "Broadcaster", "alert": false, "sound": false, "soundUrl": "", "color": "#7f427f00" }, { "name": "subscriber", "displayName": "Subscriber", "alert": false, "sound": false, "soundUrl": "", "color": "#7f7f3f49" }, { "name": "founder", "displayName": "Founder", "alert": true, "sound": false, "soundUrl": "", "color": "#7fe8b7eb" }, { "name": "vip", "displayName": "VIP", "showInMentions": true, "alert": false, "sound": false, "soundUrl": "", "color": "#7fe8b7ec" } ], "subHighlightColor": "#64ffd641" } })!"; struct TestCase { // TODO: create one of these from a raw irc message? hmm xD struct { MessageParseArgs args; std::vector badges; QString senderName; QString originalMessage; MessageFlags flags; } input; struct { bool state; HighlightResult result; } expected; }; class HighlightControllerTest : public ::testing::Test { protected: void SetUp() override { { // Write default settings to the mock settings json file QDir().mkpath("/tmp/c2-tests"); QFile settingsFile("/tmp/c2-tests/settings.json"); assert(settingsFile.open(QIODevice::WriteOnly | QIODevice::Text)); QTextStream out(&settingsFile); out << DEFAULT_SETTINGS; } this->mockHelix = new MockHelix; initializeHelix(this->mockHelix); EXPECT_CALL(*this->mockHelix, loadBlocks).Times(Exactly(1)); EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1)); this->mockApplication = std::make_unique(); this->settings = std::make_unique("/tmp/c2-tests"); this->paths = std::make_unique(); this->controller = std::make_unique(); this->mockApplication->accounts.initialize(*this->settings, *this->paths); this->controller->initialize(*this->settings, *this->paths); } void TearDown() override { QDir().rmdir("/tmp/c2-tests"); this->mockApplication.reset(); this->settings.reset(); this->paths.reset(); this->controller.reset(); delete this->mockHelix; } std::unique_ptr mockApplication; std::unique_ptr settings; std::unique_ptr paths; std::unique_ptr controller; MockHelix *mockHelix; }; TEST_F(HighlightControllerTest, A) { auto currentUser = this->mockApplication->getAccounts()->twitch.getCurrent(); std::vector tests{ { { // input MessageParseArgs{}, // no special args {}, // no badges "pajlada", // sender name "hello!", // original message }, { // expected true, // state { false, // alert false, // playsound boost::none, // custom sound url std::make_shared("#7fffffff"), // color false, }, }, }, { { // input MessageParseArgs{}, // no special args {}, // no badges "pajlada2", // sender name "hello!", // original message }, { // expected false, // state HighlightResult::emptyResult(), // result }, }, { { // input MessageParseArgs{}, // no special args { { "founder", "0", }, // founder badge }, "pajlada22", // sender name "hello!", // original message }, { // expected true, // state { true, // alert false, // playsound boost::none, // custom sound url std::make_shared("#7fe8b7eb"), // color false, //showInMentions }, }, }, { { // input MessageParseArgs{}, // no special args { { "founder", "0", }, // founder badge }, "pajlada", // sender name "hello!", // original message }, { // expected true, // state { true, // alert false, // playsound boost::none, // custom sound url std::make_shared("#7fffffff"), // color false, //showInMentions }, }, }, { // Badge highlight with showInMentions only { // input MessageParseArgs{}, // no special args { { "vip", "0", }, }, "badge", // sender name "show in mentions only", // original message }, { // expected true, // state { false, // alert false, // playsound boost::none, // custom sound url std::make_shared("#7fe8b7ec"), // color true, // showInMentions }, }, }, { // User mention with showInMentions { // input MessageParseArgs{}, // no special args {}, // no badges "gempir", // sender name "a", // original message }, { // expected true, // state { true, // alert false, // playsound boost::none, // custom sound url std::make_shared("#7ff19900"), // color true, // showInMentions }, }, }, { { // input MessageParseArgs{}, // no special args {}, // no badges "a", // sender name "!testmanxd", // original message }, { // expected true, // state { true, // alert true, // playsound boost::none, // custom sound url std::make_shared("#7f7f3f49"), // color true, // showInMentions }, }, }, { // TEST CASE: Message phrase from sender should be ignored (so showInMentions false), but since it's a user highlight, it should set a color { // input MessageParseArgs{}, // no special args {}, // no badges "testaccount_420", // sender name "!testmanxd", // original message }, { // expected true, // state { false, // alert false, // playsound boost::none, // custom sound url std::make_shared("#6fffffff"), // color false, }, }, }, }; for (const auto &[input, expected] : tests) { auto [isMatch, matchResult] = this->controller->check(input.args, input.badges, input.senderName, input.originalMessage, input.flags); EXPECT_EQ(isMatch, expected.state) << qUtf8Printable(input.senderName) << ": " << qUtf8Printable(input.originalMessage); EXPECT_EQ(matchResult, expected.result) << qUtf8Printable(input.senderName) << ": " << qUtf8Printable(input.originalMessage); } }