diff options
| author | EuAndreh <eu@euandre.org> | 2026-04-25 03:35:26 -0300 |
|---|---|---|
| committer | EuAndreh <eu@euandre.org> | 2026-04-25 03:35:26 -0300 |
| commit | e5d9c4b65b314494025325fbade831a02bae7194 (patch) | |
| tree | 002e82750c7fea9a3d2e73a6aa852f1909d8de8f /tests | |
| parent | Fix KICK default reason and nonexistent channel error (diff) | |
| download | papod-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.clj | 155 | ||||
| -rw-r--r-- | tests/unit.clj | 364 |
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.) |
