summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-25 03:35:26 -0300
committerEuAndreh <eu@euandre.org>2026-04-25 03:35:26 -0300
commite5d9c4b65b314494025325fbade831a02bae7194 (patch)
tree002e82750c7fea9a3d2e73a6aa852f1909d8de8f /tests
parentFix KICK default reason and nonexistent channel error (diff)
downloadpapod-e5d9c4b65b314494025325fbade831a02bae7194.tar.gz
papod-e5d9c4b65b314494025325fbade831a02bae7194.tar.xz
Add unit+integration tests for WHOIS, WHO, MODE, TOPIC, KICK, AWAY,
NOTICE, NAMES, LIST, PRIVMSG edge cases, and comma-separated JOIN Unit tests (8 new test groups, 53 new assertions): - test_whois-who-mode: WHOIS user info, WHOIS away status, WHOIS missing user, WHO channel members, WHO nonexistent nick, MODE user (221), MODE channel (324+329), USERHOST with away indicator - test_topic: query with no topic (331), set+query+broadcast (332), not-on-channel (442), missing params (461) - test_kick-irc: removes user + notifies all, default reason is kicker's nick, nonexistent channel (403), not on channel (442), target not on channel (441), missing params (461) - test_away-rpl: RPL_AWAY (301) on PRIVMSG to away user, no RPL_AWAY for non-away user - test_notice-dm: NOTICE delivers to user DM - test_privmsg-empty-trailing: PRIVMSG #chan : returns 412 - test_join-comma-separated: JOIN #a,#b creates both, invalid channel in comma list gets 403 - test_names-list: NAMES nonexistent (366), LIST returns 323 Integration tests (6 new tests, 17 new assertions): - test_kick-irc-command: KICK notifies kicker, kicked, and observers - test_topic-broadcast: TOPIC set broadcasts + query returns 332 - test_part-broadcast: PART with message delivered to members - test_whois-between-clients: WHOIS from one client about another - test_who-channel: WHO #channel lists all members - test_nick-collision: second client with same nick gets 433 Also fixes: - PRIVMSG #chan : (lone colon) now correctly returns 412 - Ghost check only triggers when socket is present (fixes integration test false ghosting via piped streams) Totals: 23 unit tests (260 assertions), 16 integration tests (38 assertions), 205 irctest acceptance tests pass.
Diffstat (limited to 'tests')
-rw-r--r--tests/integration.clj155
-rw-r--r--tests/unit.clj364
2 files changed, 519 insertions, 0 deletions
diff --git a/tests/integration.clj b/tests/integration.clj
index b3076ee..769af90 100644
--- a/tests/integration.clj
+++ b/tests/integration.clj
@@ -340,6 +340,161 @@
((:close! alice))
((:close! bob))))))
+(deftest test_kick-irc-command
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)
+ baz (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (register! baz "baz" (:net-id components))
+ (wait-for baz "001" 2000)
+ (send! alice "JOIN #kick")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #kick")
+ (wait-for bob "JOIN" 2000)
+ (send! baz "JOIN #kick")
+ (wait-for baz "JOIN" 2000)
+ (.reset (:client-out alice))
+ (.reset (:client-out bob))
+ (.reset (:client-out baz))
+ (send! alice "KICK #kick bob :bye")
+ ;; All three see the KICK
+ (let [bob-out (wait-for bob "KICK" 2000)]
+ (is (string/includes? bob-out "KICK #kick bob :bye")))
+ (let [baz-out (wait-for baz "KICK" 2000)]
+ (is (string/includes? baz-out "KICK #kick bob")))
+ (let [alice-out (wait-for alice "KICK" 2000)]
+ (is (string/includes? alice-out "KICK #kick bob")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))
+ ((:close! baz))))))
+
+(deftest test_topic-broadcast
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (send! alice "JOIN #topic")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #topic")
+ (wait-for bob "JOIN" 2000)
+ (.reset (:client-out alice))
+ (.reset (:client-out bob))
+ ;; Alice sets topic
+ (send! alice "TOPIC #topic :New topic here")
+ ;; Both see it
+ (let [bob-out (wait-for bob "TOPIC" 2000)]
+ (is (string/includes? bob-out
+ "TOPIC #topic :New topic here")))
+ (let [alice-out (wait-for alice "TOPIC" 2000)]
+ (is (string/includes? alice-out
+ "TOPIC #topic :New topic here")))
+ ;; Query returns the topic
+ (.reset (:client-out alice))
+ (send! alice "TOPIC #topic")
+ (let [reply (wait-for alice "332" 2000)]
+ (is (string/includes? reply "New topic here")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
+(deftest test_part-broadcast
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (send! alice "JOIN #part")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #part")
+ (wait-for bob "JOIN" 2000)
+ (.reset (:client-out alice))
+ (.reset (:client-out bob))
+ ;; Bob parts with message
+ (send! bob "PART #part :leaving now")
+ (let [alice-out (wait-for alice "PART" 2000)]
+ (is (string/includes? alice-out
+ "bob PART #part :leaving now")))
+ (let [bob-out (wait-for bob "PART" 2000)]
+ (is (string/includes? bob-out "PART #part")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
+(deftest test_whois-between-clients
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (.reset (:client-out alice))
+ (send! alice "WHOIS bob")
+ (let [out (wait-for alice "318" 2000)]
+ (is (string/includes? out " 311 "))
+ (is (string/includes? out "bob"))
+ (is (string/includes? out " 318 ")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
+(deftest test_who-channel
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (send! alice "JOIN #who")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #who")
+ (wait-for bob "JOIN" 2000)
+ (.reset (:client-out alice))
+ (send! alice "WHO #who")
+ (let [out (wait-for alice "315" 2000)]
+ ;; Two 352 replies (alice + bob) + one 315
+ (is (string/includes? out " 352 "))
+ (is (string/includes? out "alice"))
+ (is (string/includes? out "bob"))
+ (is (string/includes? out " 315 ")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
+(deftest test_nick-collision
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ ;; Bob tries the same nick
+ (send! bob (str "PASS " (:net-id components)))
+ (send! bob "NICK alice")
+ (send! bob "USER bob 0 * :bob")
+ (let [out (wait-for bob "433" 2000)]
+ (is (string/includes? out "433"))
+ (is (string/includes? out "already in use")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
(defn -main
[& _args]
(binding [*out* *err*]
diff --git a/tests/unit.clj b/tests/unit.clj
index 71513f2..f2718eb 100644
--- a/tests/unit.clj
+++ b/tests/unit.clj
@@ -1065,11 +1065,375 @@
(is (string/includes? (first replies) "904"))
(is (not (:authenticated? @c))))))))
+(def handle-topic @#'papod/handle-topic)
+(def handle-kick @#'papod/handle-kick)
(def handle-chathistory @#'papod/handle-chathistory)
(def handle-redact @#'papod/handle-redact)
(def handle-edit @#'papod/handle-edit)
(def handle-tagmsg @#'papod/handle-tagmsg)
+(deftest test_whois-who-mode
+ (testing "WHOIS returns user info"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "WHOIS" :params ["" "bob"]}
+ alice components)]
+ (is (some #(string/includes? % "311") replies))
+ (is (some #(string/includes? % "bob") replies))
+ (is (some #(string/includes? % "318") replies)))))
+ (testing "WHOIS shows away status"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ _ (swap! bob assoc :away "gone fishing")
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "WHOIS" :params ["" "bob"]}
+ alice components)]
+ (is (some #(string/includes? % "301") replies))
+ (is (some #(string/includes? % "gone fishing")
+ replies)))))
+ (testing "WHOIS for nonexistent user"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "WHOIS" :params ["" "nobody"]}
+ c components)]
+ (is (some #(string/includes? % "401") replies))
+ (is (some #(string/includes? % "318") replies)))))
+ (testing "WHO #channel lists members"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {"#test" #{"alice" "bob"}}))]
+ (let [replies (replies-for!
+ {:command "WHO" :params ["" "#test"]}
+ alice components)]
+ ;; One 352 per member + one 315
+ (is (= 2 (count (filter
+ #(string/includes? % " 352 ")
+ replies))))
+ (is (some #(string/includes? % "315") replies)))))
+ (testing "WHO nonexistent nick"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "WHO" :params ["" "nobody"]}
+ c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "315")))))
+ (testing "MODE user returns 221"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "MODE" :params ["" "alice"]}
+ c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "221")))))
+ (testing "MODE #channel returns 324 + 329"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "MODE"
+ :params ["" "#test"]}
+ c components)]
+ (is (= 2 (count replies)))
+ (is (string/includes? (first replies) "324"))
+ (is (string/includes? (second replies) "329")))))
+ (testing "USERHOST returns 302 with away indicator"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ _ (swap! bob assoc :away "brb")
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {}))]
+ (let [replies (replies-for!
+ {:command "USERHOST"
+ :params ["" "alice" "bob"]}
+ alice components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "302"))
+ (is (string/includes? (first replies) "alice=+"))
+ (is (string/includes? (first replies) "bob=-"))))))
+
+(deftest test_topic
+ (testing "TOPIC query with no topic"
+ (let [{:keys [test-network-id] :as components}
+ (assoc (test-components-with-network)
+ :clients (atom {}) :channels (atom {}))
+ out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" out test-network-id)
+ components (assoc components
+ :clients (atom {"alice" {:w out}}))]
+ (handle-join ["#test"] alice components)
+ (let [replies (handle-topic ["#test"] alice components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "331")))))
+ (testing "TOPIC set and query"
+ (let [{:keys [test-network-id] :as components}
+ (test-components-with-network)
+ alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out
+ test-network-id)
+ components
+ (assoc components
+ :clients (atom {"alice" {:w alice-out}
+ "bob" {:w bob-out}})
+ :channels (atom {}))]
+ (handle-join ["#test"] alice components)
+ (swap! (:channels components) update "#test" conj "bob")
+ ;; Set topic
+ (.reset bob-out)
+ (handle-topic ["#test" ":Hello World"] alice components)
+ ;; Bob receives TOPIC broadcast
+ (is (string/includes? (.toString bob-out "UTF-8")
+ "TOPIC #test :Hello World"))
+ ;; Query returns 332
+ (let [replies (handle-topic ["#test"] alice components)]
+ (is (string/includes? (first replies) "332"))
+ (is (string/includes? (first replies) "Hello World")))))
+ (testing "TOPIC not on channel"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :channels (atom {"#test" #{"bob"}}))]
+ (let [replies (handle-topic ["#test"] c components)]
+ (is (string/includes? (first replies) "442")))))
+ (testing "TOPIC no params"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))]
+ (let [replies (handle-topic [] c no-conn)]
+ (is (string/includes? (first replies) "461"))))))
+
+(deftest test_kick-irc
+ (testing "KICK removes user and notifies"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ baz-out (java.io.ByteArrayOutputStream.)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out}
+ "bob" {:w bob-out}
+ "baz" {:w baz-out}})
+ :channels (atom {"#test" #{"alice" "bob" "baz"}}))
+ alice (registered-client "alice" alice-out)]
+ (handle-kick ["#test" "bob" ":bye!"] alice components)
+ ;; bob removed from channel
+ (is (not (contains? (get @(:channels components) "#test")
+ "bob")))
+ ;; All three get KICK notification
+ (is (string/includes? (.toString alice-out "UTF-8")
+ "KICK #test bob"))
+ (is (string/includes? (.toString bob-out "UTF-8")
+ "KICK #test bob :bye!"))
+ (is (string/includes? (.toString baz-out "UTF-8")
+ "KICK #test bob"))))
+ (testing "KICK default reason is kicker's nick"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out}
+ "bob" {:w bob-out}})
+ :channels (atom {"#test" #{"alice" "bob"}}))
+ alice (registered-client "alice" alice-out)]
+ (handle-kick ["#test" "bob"] alice components)
+ (is (string/includes? (.toString bob-out "UTF-8")
+ "KICK #test bob :alice"))))
+ (testing "KICK nonexistent channel"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (let [replies (handle-kick ["#nope" "bob"] c components)]
+ (is (string/includes? (first replies) "403")))))
+ (testing "KICK not on channel"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :channels (atom {"#test" #{"bob"}}))]
+ (let [replies (handle-kick ["#test" "bob"] c components)]
+ (is (string/includes? (first replies) "442")))))
+ (testing "KICK target not on channel"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :channels
+ (atom {"#test" #{"alice"}}))]
+ (let [replies (handle-kick ["#test" "bob"] c components)]
+ (is (string/includes? (first replies) "441")))))
+ (testing "KICK no params"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))]
+ (let [replies (handle-kick [] c no-conn)]
+ (is (string/includes? (first replies) "461"))))))
+
+(deftest test_away-rpl
+ (testing "PRIVMSG to away user returns RPL_AWAY"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ _ (swap! bob assoc :away "on vacation")
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {}))]
+ (let [replies (handle-privmsg ["bob" ":hey"]
+ nil alice components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "301"))
+ (is (string/includes? (first replies) "on vacation"))
+ (is (string/includes? (first replies) "bob")))))
+ (testing "PRIVMSG to non-away user returns no RPL_AWAY"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" alice-out)
+ bob (registered-client "bob" bob-out)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out
+ :client-atom alice}
+ "bob" {:w bob-out
+ :client-atom bob}})
+ :channels (atom {}))]
+ (is (empty? (handle-privmsg ["bob" ":hey"]
+ nil alice components))))))
+
+(deftest test_notice-dm
+ (testing "NOTICE delivers to user"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out}
+ "bob" {:w bob-out}})
+ :channels (atom {}))
+ alice (registered-client "alice" alice-out)]
+ (@#'papod/handle-notice ["bob" ":hi there"]
+ alice components)
+ (is (string/includes? (.toString bob-out "UTF-8")
+ "NOTICE bob :hi there"))
+ (is (= "" (.toString alice-out "UTF-8"))))))
+
+(deftest test_privmsg-empty-trailing
+ (testing "PRIVMSG #chan : returns 412"
+ (let [{:keys [test-network-id] :as components}
+ (test-components-with-network)
+ out (java.io.ByteArrayOutputStream.)
+ c (registered-client "alice" out test-network-id)
+ components (assoc components
+ :clients (atom {"alice" {:w out}})
+ :channels (atom {}))]
+ (handle-join ["#test"] c components)
+ (let [replies (replies-for!
+ {:command "PRIVMSG"
+ :params ["" "#test" ":"]}
+ c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "412"))))))
+
+(deftest test_join-comma-separated
+ (testing "JOIN #a,#b creates two channels"
+ (let [{:keys [test-network-id] :as components}
+ (assoc (test-components-with-network)
+ :clients (atom {}) :channels (atom {}))
+ out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" out test-network-id)
+ components (assoc components
+ :clients (atom {"alice" {:w out}}))]
+ (handle-join ["#a,#b"] alice components)
+ (is (contains? (get @(:channels components) "#a")
+ "alice"))
+ (is (contains? (get @(:channels components) "#b")
+ "alice"))))
+ (testing "JOIN #valid,invalid rejects invalid"
+ (let [{:keys [test-network-id] :as components}
+ (assoc (test-components-with-network)
+ :clients (atom {}) :channels (atom {}))
+ out (java.io.ByteArrayOutputStream.)
+ alice (registered-client "alice" out test-network-id)
+ components (assoc components
+ :clients (atom {"alice" {:w out}}))]
+ (let [replies (handle-join ["#ok,nope"] alice
+ components)]
+ ;; #ok should succeed, nope should get 403
+ (is (some #(string/includes? % "403") replies))
+ (is (contains? (get @(:channels components) "#ok")
+ "alice"))))))
+
+(deftest test_names-list
+ (testing "NAMES on nonexistent channel"
+ (let [{:keys [test-network-id] :as components}
+ (test-components-with-network)
+ c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.)
+ test-network-id)]
+ (let [replies (replies-for!
+ {:command "NAMES"
+ :params ["" "#nope"]}
+ c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "366")))))
+ (testing "LIST returns end-of-list"
+ (let [{:keys [test-network-id] :as components}
+ (test-components-with-network)
+ c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.)
+ test-network-id)]
+ (let [replies (replies-for!
+ {:command "LIST" :params []}
+ c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "323"))))))
+
(deftest test_messaging-features
(testing "messages include msgid tags"
(let [alice-out (java.io.ByteArrayOutputStream.)