#include #include #include #include "../memory_store.h" #include "irc_session.h" using namespace roro; void setUp() {} void tearDown() {} struct Fixture { MemoryStore store; IrcConfig config{store}; std::unique_ptr session; IrcEffects fx; explicit Fixture(bool sasl = false) { config.load("roro"); config.nick = "roro"; config.autojoin = {{"#roro", ""}}; if (sasl) { config.saslUser = "acct"; config.saslPassword = "pw"; } session.reset(new IrcSession(config)); } void recv(const std::string& line) { session->receive(line, 1000); } void tick(uint32_t ms) { session->tick(ms); } IrcEffects& take() { fx = session->takeEffects(); return fx; } bool sent(const std::string& line) { return std::find(fx.send.begin(), fx.send.end(), line) != fx.send.end(); } void registerNow() { session->connected(0); recv(":srv 001 roro :Welcome"); take(); } int buffer(const std::string& name) { for (int i = 0; i < session->bufferCount(); i++) if (session->buffer(i).name == name) return i; return -1; } }; void test_plain_registration_sends_nick_and_user() { Fixture f; f.session->connected(0); f.take(); TEST_ASSERT_TRUE(f.sent("NICK roro")); TEST_ASSERT_TRUE(f.sent("USER roro 0 * roro9stack")); } void test_welcome_registers_and_joins_autojoin_channels() { Fixture f; f.session->connected(0); f.take(); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.session->registered()); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); } void test_nickserv_identify_after_welcome_and_joins_wait_for_it() { Fixture f; f.config.nickservPassword = "pw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.tick(0); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw")); TEST_ASSERT_FALSE(f.sent("JOIN #roro")); // not before NickServ has had its say f.tick(1000); TEST_ASSERT_TRUE(f.take().send.empty()); } void test_joins_follow_the_logged_in_reply() { Fixture f; f.config.nickservPassword = "pw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.tick(0); f.recv(":srv 001 roro :Welcome"); f.take(); f.tick(300); f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); f.tick(5000); TEST_ASSERT_TRUE(f.take().send.empty()); // and only once } void test_joins_go_ahead_after_two_seconds_without_a_reply() { Fixture f; f.config.nickservPassword = "pw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.tick(10000); f.recv(":srv 001 roro :Welcome"); f.take(); f.tick(11999); TEST_ASSERT_TRUE(f.take().send.empty()); f.tick(12000); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); } void test_failed_sasl_falls_back_to_nickserv_and_holds_joins() { Fixture f(true); f.config.nickservPassword = "nspw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.recv(":srv CAP * ACK :sasl"); f.recv("AUTHENTICATE +"); f.recv(":srv 904 roro :SASL authentication failed"); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw")); TEST_ASSERT_FALSE(f.sent("JOIN #roro")); f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); } void test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account() { Fixture f(true); // SASL user "acct", password "pw" f.session->connected(0); f.recv(":srv 904 roro :SASL authentication failed"); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct pw")); } void test_successful_sasl_joins_at_once() { Fixture f(true); f.config.nickservPassword = "nspw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.recv(":srv 903 roro :SASL authentication successful"); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); TEST_ASSERT_FALSE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw")); } void test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it() { Fixture f; f.config.nickservPassword = "pw"; f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.recv(":srv 433 * roro :Nickname is already in use"); // a stale session still holds it f.recv(":srv 001 roro_ :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw")); f.recv(":srv 900 roro_ roro_!u@h roro :You are now logged in as roro"); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :REGAIN roro")); TEST_ASSERT_TRUE(f.sent("JOIN #roro")); } void test_autojoin_sends_keyed_channels_first_with_their_keys() { Fixture f; f.config.autojoin = IrcConfig::parseChannels("#public, #private s3cret"); f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #private,#public s3cret")); } void test_keys_used_with_join_are_reused_on_rejoin() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.session->input(1, "/join #vault k3y", 0); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #vault k3y")); f.recv(":roro!u@h JOIN #vault"); f.session->disconnected(0, "Wi-Fi lost"); f.session->connected(0); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #vault,#roro k3y")); } void test_sasl_plain_flow() { Fixture f(true); f.session->connected(0); f.take(); TEST_ASSERT_TRUE(f.sent("CAP REQ :sasl")); f.recv(":srv CAP * ACK :sasl"); f.take(); TEST_ASSERT_TRUE(f.sent("AUTHENTICATE PLAIN")); f.recv("AUTHENTICATE +"); f.take(); TEST_ASSERT_TRUE(f.sent("AUTHENTICATE YWNjdABhY2N0AHB3")); // acct\0acct\0pw f.recv(":srv 903 roro :SASL authentication successful"); f.take(); TEST_ASSERT_TRUE(f.sent("CAP END")); } void test_sasl_failure_still_ends_negotiation_and_says_so() { Fixture f(true); f.session->connected(0); f.recv(":srv CAP * ACK :sasl"); f.recv("AUTHENTICATE +"); f.take(); f.recv(":srv 904 roro :SASL authentication failed"); f.take(); TEST_ASSERT_TRUE(f.sent("CAP END")); const auto& server = f.session->buffer(0); TEST_ASSERT_TRUE(server.lines.back().text.find("SASL") != std::string::npos); } void test_nick_in_use_during_registration_tries_another() { Fixture f; f.session->connected(0); f.take(); f.recv(":srv 433 * roro :Nickname is already in use"); f.take(); TEST_ASSERT_TRUE(f.sent("NICK roro_")); } void test_ping_is_answered() { Fixture f; f.registerNow(); f.recv("PING :tungsten.libera.chat"); f.take(); TEST_ASSERT_TRUE(f.sent("PONG tungsten.libera.chat")); } void test_own_join_creates_a_channel_buffer() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); int b = f.buffer("#roro"); TEST_ASSERT_TRUE(b > 0); TEST_ASSERT_TRUE(f.session->buffer(b).joined); } void test_others_joining_is_hidden() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); size_t before = f.session->buffer(f.buffer("#roro")).lines.size(); f.recv(":bob!u@h JOIN #roro"); f.recv(":bob!u@h PART #roro"); f.recv(":bob!u@h QUIT :bye"); TEST_ASSERT_EQUAL(before, f.session->buffer(f.buffer("#roro")).lines.size()); } void test_channel_message_goes_to_its_buffer_and_is_logged() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.take(); f.recv(":alice!u@h PRIVMSG #roro :hello all"); auto& fx = f.take(); const auto& b = f.session->buffer(f.buffer("#roro")); TEST_ASSERT_EQUAL_STRING("alice", b.lines.back().nick.c_str()); TEST_ASSERT_EQUAL_STRING("hello all", b.lines.back().text.c_str()); TEST_ASSERT_EQUAL(1, fx.logs.size()); TEST_ASSERT_EQUAL_STRING("#roro", fx.logs[0].buffer.c_str()); } void test_unread_counts_unless_viewing_that_buffer() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); int b = f.buffer("#roro"); f.recv(":alice!u@h PRIVMSG #roro :one"); f.recv(":alice!u@h PRIVMSG #roro :two"); TEST_ASSERT_EQUAL(2, f.session->buffer(b).unread); TEST_ASSERT_EQUAL(2, f.session->totalUnread()); f.session->setViewing(b); TEST_ASSERT_EQUAL(0, f.session->buffer(b).unread); f.recv(":alice!u@h PRIVMSG #roro :three"); TEST_ASSERT_EQUAL(0, f.session->buffer(b).unread); } void test_mention_notifies_ordinary_traffic_does_not() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.take(); f.recv(":alice!u@h PRIVMSG #roro :lunch?"); TEST_ASSERT_EQUAL(0, f.take().notifications.size()); f.recv(":alice!u@h PRIVMSG #roro :Roro: ping"); TEST_ASSERT_EQUAL(1, f.take().notifications.size()); f.recv(":alice!u@h PRIVMSG #roro :rorojam is not you"); TEST_ASSERT_EQUAL(0, f.take().notifications.size()); } void test_private_message_opens_a_query_buffer_and_notifies() { Fixture f; f.registerNow(); f.recv(":alice!u@h PRIVMSG roro :psst"); TEST_ASSERT_TRUE(f.buffer("alice") > 0); TEST_ASSERT_EQUAL(1, f.take().notifications.size()); } void test_no_notification_while_viewing_that_buffer() { Fixture f; f.registerNow(); f.recv(":alice!u@h PRIVMSG roro :first"); f.take(); f.session->setViewing(f.buffer("alice")); f.recv(":alice!u@h PRIVMSG roro :second"); TEST_ASSERT_EQUAL(0, f.take().notifications.size()); } void test_action_and_ctcp_version() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.recv(":alice!u@h PRIVMSG #roro :\x01" "ACTION waves\x01"); const auto& line = f.session->buffer(f.buffer("#roro")).lines.back(); TEST_ASSERT_EQUAL(static_cast(IrcLine::Kind::Action), static_cast(line.kind)); TEST_ASSERT_EQUAL_STRING("waves", line.text.c_str()); f.take(); f.recv(":alice!u@h PRIVMSG roro :\x01VERSION\x01"); f.take(); TEST_ASSERT_TRUE(f.sent("NOTICE alice :\x01VERSION roro9stack\x01")); } void test_topic_is_kept() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.recv(":srv 332 roro #roro :Welcome to roro"); TEST_ASSERT_EQUAL_STRING("Welcome to roro", f.session->buffer(f.buffer("#roro")).topic.c_str()); } void test_input_message_and_commands() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); int b = f.buffer("#roro"); f.take(); f.session->input(b, "hello", 0); f.session->input(b, "/me waves", 0); f.session->input(b, "/topic New topic", 0); f.session->input(b, "/join #other", 0); f.session->input(b, "/part", 0); f.session->input(b, "/nick roro2", 0); f.session->input(b, "/names", 0); f.session->input(b, "/raw MODE #roro +t", 0); f.session->input(b, "//not a command", 0); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG #roro hello")); TEST_ASSERT_TRUE(f.sent("PRIVMSG #roro :\x01" "ACTION waves\x01")); TEST_ASSERT_TRUE(f.sent("TOPIC #roro :New topic")); TEST_ASSERT_TRUE(f.sent("JOIN #other")); TEST_ASSERT_TRUE(f.sent("PART #roro")); TEST_ASSERT_TRUE(f.sent("NICK roro2")); TEST_ASSERT_TRUE(f.sent("NAMES #roro")); TEST_ASSERT_TRUE(f.sent("MODE #roro +t")); TEST_ASSERT_TRUE(f.sent("PRIVMSG #roro :/not a command")); // Own messages appear in the buffer TEST_ASSERT_EQUAL_STRING("/not a command", f.session->buffer(b).lines.back().text.c_str()); } void test_j_is_short_for_join() { Fixture f; f.registerNow(); f.session->input(0, "/j #short", 0); f.session->input(0, "/j #locked k3y", 0); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #short")); TEST_ASSERT_TRUE(f.sent("JOIN #locked k3y")); } void test_msg_opens_a_query_buffer() { Fixture f; f.registerNow(); f.session->input(0, "/msg alice hi there", 0); f.take(); TEST_ASSERT_TRUE(f.sent("PRIVMSG alice :hi there")); TEST_ASSERT_TRUE(f.buffer("alice") > 0); } void test_text_in_the_server_buffer_explains_instead_of_sending() { Fixture f; f.registerNow(); f.session->input(0, "hello", 0); TEST_ASSERT_TRUE(f.take().send.empty()); TEST_ASSERT_TRUE(f.session->buffer(0).lines.back().text.find("/join") != std::string::npos); } void test_quit_sends_and_is_remembered() { Fixture f; f.registerNow(); f.session->input(0, "/quit bye", 0); f.take(); TEST_ASSERT_TRUE(f.sent("QUIT bye")); TEST_ASSERT_TRUE(f.session->quitRequested()); } void test_names_reply_only_shown_when_asked() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); int b = f.buffer("#roro"); size_t before = f.session->buffer(b).lines.size(); f.recv(":srv 353 roro = #roro :roro alice bob"); TEST_ASSERT_EQUAL(before, f.session->buffer(b).lines.size()); f.session->input(b, "/names", 0); f.recv(":srv 353 roro = #roro :roro alice bob"); TEST_ASSERT_TRUE(f.session->buffer(b).lines.back().text.find("alice bob") != std::string::npos); } void test_disconnect_marks_a_gap_and_reconnect_rejoins() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); f.session->input(1, "/join #extra", 0); f.recv(":roro!u@h JOIN #extra"); f.session->disconnected(5000, "paused for Wi-Fi monitoring"); TEST_ASSERT_FALSE(f.session->registered()); TEST_ASSERT_TRUE(f.session->buffer(f.buffer("#roro")).lines.back().text.find("paused") != std::string::npos); f.take(); f.session->connected(6000); f.recv(":srv 001 roro :Welcome"); f.take(); TEST_ASSERT_TRUE(f.sent("JOIN #roro,#extra")); } void test_buffer_keeps_only_recent_lines() { Fixture f; f.registerNow(); f.recv(":roro!u@h JOIN #roro"); for (int i = 0; i < IrcSession::kLinesPerBuffer + 10; i++) f.recv(":a!u@h PRIVMSG #roro :x"); TEST_ASSERT_EQUAL(IrcSession::kLinesPerBuffer, f.session->buffer(f.buffer("#roro")).lines.size()); } void test_quit_is_forgotten_once_disconnected() { Fixture f; f.registerNow(); f.session->input(0, "/quit", 0); f.session->disconnected(0, "disconnected"); TEST_ASSERT_FALSE(f.session->quitRequested()); } void test_server_buffer_never_counts_as_unread() { Fixture f; f.session->connected(0); f.recv(":srv NOTICE * :*** Looking up your hostname..."); f.recv(":srv 372 roro :- Message of the day"); f.recv(":srv 001 roro :Welcome"); TEST_ASSERT_EQUAL(0, f.session->totalUnread()); } void test_revision_changes_when_something_visible_changes() { Fixture f; f.registerNow(); uint32_t r = f.session->revision(); f.recv("PING :x"); // nothing visible TEST_ASSERT_EQUAL_UINT32(r, f.session->revision()); f.recv(":roro!u@h JOIN #roro"); TEST_ASSERT_TRUE(f.session->revision() != r); } int main() { UNITY_BEGIN(); RUN_TEST(test_plain_registration_sends_nick_and_user); RUN_TEST(test_welcome_registers_and_joins_autojoin_channels); RUN_TEST(test_nickserv_identify_after_welcome_and_joins_wait_for_it); RUN_TEST(test_joins_follow_the_logged_in_reply); RUN_TEST(test_joins_go_ahead_after_two_seconds_without_a_reply); RUN_TEST(test_failed_sasl_falls_back_to_nickserv_and_holds_joins); RUN_TEST(test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account); RUN_TEST(test_successful_sasl_joins_at_once); RUN_TEST(test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it); RUN_TEST(test_autojoin_sends_keyed_channels_first_with_their_keys); RUN_TEST(test_keys_used_with_join_are_reused_on_rejoin); RUN_TEST(test_sasl_plain_flow); RUN_TEST(test_sasl_failure_still_ends_negotiation_and_says_so); RUN_TEST(test_nick_in_use_during_registration_tries_another); RUN_TEST(test_ping_is_answered); RUN_TEST(test_own_join_creates_a_channel_buffer); RUN_TEST(test_others_joining_is_hidden); RUN_TEST(test_channel_message_goes_to_its_buffer_and_is_logged); RUN_TEST(test_unread_counts_unless_viewing_that_buffer); RUN_TEST(test_mention_notifies_ordinary_traffic_does_not); RUN_TEST(test_private_message_opens_a_query_buffer_and_notifies); RUN_TEST(test_no_notification_while_viewing_that_buffer); RUN_TEST(test_action_and_ctcp_version); RUN_TEST(test_topic_is_kept); RUN_TEST(test_input_message_and_commands); RUN_TEST(test_j_is_short_for_join); RUN_TEST(test_msg_opens_a_query_buffer); RUN_TEST(test_text_in_the_server_buffer_explains_instead_of_sending); RUN_TEST(test_quit_sends_and_is_remembered); RUN_TEST(test_names_reply_only_shown_when_asked); RUN_TEST(test_disconnect_marks_a_gap_and_reconnect_rejoins); RUN_TEST(test_buffer_keeps_only_recent_lines); RUN_TEST(test_quit_is_forgotten_once_disconnected); RUN_TEST(test_server_buffer_never_counts_as_unread); RUN_TEST(test_revision_changes_when_something_visible_changes); return UNITY_END(); }